@docspack/lapis
v0.2.0
Published
Convert an OpenAPI 3 document to LAPIS, the compact notation for describing an API to a language model. About 90% fewer tokens than the same document as JSON.
Maintainers
Readme
@docspack/lapis
OpenAPI 3 in, LAPIS out. About 90% fewer tokens than the same document as JSON.
LAPIS is an open notation for describing an API to a language model rather than to a code generator: a signature per operation, compact types, and the sections OpenAPI spends thousands of lines on collapsed into a few. There was no JavaScript converter for it, so this is one.
npx @docspack/lapis ./openapi.yaml
npx @docspack/lapis ./openapi.json -o api.lapis --stats
curl -s https://example.com/openapi.json | npx @docspack/lapis - > api.lapis[meta]
api: Invoice Service
base: https://api.example.com/v2
auth: bearer header:Authorization
[types]
InvoiceCreate:
customer_id: str
due_date?: date
[ops]
create_invoice POST /invoices
Creates an invoice for a customer.
> body: InvoiceCreate
< Invoice # 201 The created invoice.
@auth bearerAuth
[errors]
401 ApiError
The token is missing, expired or invalid.Measured against Stripe's published document — 594 operations, 1,454 schemas, 8.0 MB:
594 operations, 1454 types, read as json
~193,593 tokens of LAPIS, against ~2,163,446 of JSON (91% smaller)As a library
import { parseOpenApi, toLapis } from "@docspack/lapis";
const document = parseOpenApi(await someOpenApiObject);
toLapis(document);parseOpenApi resolves every $ref, merges allOf, and keeps named schemas by name so a
recursive document terminates. It takes an already-parsed object, so this entry point has no
dependencies and runs anywhere — including a browser.
Reading a file, or text that might be YAML, is a separate entry point:
import { readApiFile, readApiText } from "@docspack/lapis/read";
const { document, format } = await readApiFile("./openapi.yaml"); // format: "json" | "yaml"That split is the point of the package's shape. JSON.parse is built into every JavaScript runtime
at zero bytes; YAML needs a parser, and that parser is ~57 KB gzipped. Putting it behind /read
means a browser bundling parseOpenApi and toLapis ships none of it, and tests/entry.test.ts
asserts that over the built output rather than trusting the comment.
The model
Both renderings come from one shape — ApiDocument, with operations, types, servers,
securitySchemes and tags. It is exported, so it is also usable on its own if you want a normalized
OpenAPI document and not LAPIS at all. toGroups(document) buckets operations by tag for a sidebar.
Three limits, and the measurement behind each
A faithful renderer produces unusable output on real documents. Each of these exists because its absence was measured against Stripe's spec:
| Limit | Without it | | --- | --- | | Type closure stops at a token budget | Every Stripe object has an expandable field pointing at another, so one operation reaches most of the API: 66,500 tokens for one endpoint | | Enumerations list 8 members | The merchant-category enum has 297, four times in one operation: 25,637 characters on one line | | Inline objects expand 2 levels, 12 fields | A document that names no schemas renders its whole body tree inline |
Everything elided is counted — +32 more, {...} — because "there is more to this shape" and "this
is the whole shape" are different claims, and only one of them is true.
Conformance
One thing here is not in LAPIS's grammar: an @auth <scheme> line on an operation. LAPIS declares
authentication in [meta] and has no per-operation form, so a document where some endpoints are
public and others are not — which is most of them — cannot say so in conformant LAPIS. It is emitted
by default because that is worth more to a model than conformance is, and turned off by --strict
(or toLapis(document, { strict: true })) for anything feeding a parser that would reject it.
Everything else is legal LAPIS: # comments carry the success status, and [errors] is the format's
own section — declared once per document, with @ops: bindings where an error is not universal.
What it refuses
Each of these stops with the command to run:
- Swagger 2.0 —
npx swagger2openapi openapi.json -o openapi3.json - An external
$ref—npx @redocly/cli bundle openapi.yaml -o openapi.json. Refused rather than dropped: a schema silently replaced by "unknown" is a description claiming an endpoint takes no body. - A
$refthat points at nothing — named, with the pointer.
Who uses it
This package is the shared half of docspack's OpenAPI support. @docspack/openapi builds on it —
per-operation digests, request building, code samples, a try-it console — and docspack build
--openapi turns a document into a searchable documentation package. None of that is needed to use
this; it converts one format to another and stops.
Licence
MIT. The LAPIS specification is CC BY 4.0 and belongs to its authors; this package implements a renderer for it.
