Skip to content

OpenAPI 3.1 coverage

What bin/altair openapi:import (OpenAPI → Altair) and spec:emit-openapi (Altair → OpenAPI) actually do with each OpenAPI feature. This is the honest, evidence-based map — kept current as #214 closes the gaps.

Standard. Map everything representable; surface (warn/error) everything else — never silently drop; emit spec-compliant OpenAPI; keep the round-trip stable. Literal 100% fidelity into generated code isn’t a goal — some features (callbacks, links, oneOf polymorphism, free-form additionalProperties) have no clean Altair representation.

  • Paths + operations; operationId (or synthesised), summary.
  • Resource naming (Phase 5) — RPC-style path leaves (findByStatus, uploadImage, login) are recognised as actions, not resources: GET /pet/findByStatus derives the Pet namespace (App\Pet\FindPetsByStatus), not a mangled FindByStatu. The nearest noun segment is the resource.
  • Parameters — path / query / header / cookie become inputs tagged with their in: location (with their declared type + required; enums → in: rule). A path param declared with schema: {type: integer} imports as int, not string. Phase 2.
  • Request + response bodies in application/json. When a request body has no application/json, its schema is read from the first content type carrying an object/array schema (multipart/form-data, x-www-form-urlencoded) — the body is normalized, not dropped (Phase 4a). Responses fall back to the first content type with any schema. On export the body always re-emits as application/json, so the wire content type is normalized while the body structure round-trips.
  • Schema types: object (incl. nested objects), array, arrays of objects, top-level array bodies, scalars (integerint, numberfloat, booleanbool, string), enumin: rule.
  • Validation constraints ↔ rules (Phase 3): format (email/uri/ip/date-time)→email/url/ip/datetime; minLength/maxLengthmin/max; minimum/maximummin/max; patternregex:. Round-trips (the forward emitter writes the inverse).
  • Internal $ref (#/components/schemas/<Name>), properties + required.
  • External relative-file $ref (Phase 4e) — a $ref into a sibling file (./schemas/pet.yaml#/Pet) is bundled: the target schema (and its own nested/internal refs) is inlined into components/schemas and the ref rewritten to internal, so multi-file specs import instead of failing. Bounded (file count, per-file size, ref-chase depth; cycles dedupe). Remote refs are never fetched and a ref escaping the document’s directory is rejected — both are warned and left unresolved.
  • allOf composition (Phase 4b) — subschemas are resolved ($refs included) and their properties merged into one object; a property required in any subschema is required in the merge. Altair has no representation for composition, so the relationship is flattened (warned) while the data is preserved. A subschema that does not resolve to an object is unmappable.
  • x-altair-* extensions (domain/persistence/queue/idempotency/webhook round-trip).

Surfaced as warnings (imported, but reported — not silent) — Phase 1

Section titled “Surfaced as warnings (imported, but reported — not silent) — Phase 1”

openapi:import warns about each of these in its receipt (warnings[]) and human output:

  • parameter $ref (not resolved).
  • non-application/json request bodies whose schema is read — when JSON is present the other representations are ignored; when it is absent an object/array body is normalized (Phase 4a). Either way the importer says which content type(s) it read or ignored.
  • binary/scalar-only request bodies (application/octet-stream, text/plain) — no named-field representation, so still dropped (warned).
  • non-application/json responses whose schema is read — when a response has no application/json, its schema is normalized from the first content type carrying one, and the importer reports the normalization (Phase 4a). When application/json is present the other representations are alternative views, not a loss, so they are not warned.
  • requestBody $ref (body dropped).
  • allOf flattening (Phase 4b) — reported once per named component that uses it (`components.schemas.<Name>` uses allOf) and per inline body/response that uses it, since the composition relationship is not preserved.
  • oneOf / anyOf (Phase 4c) — a union has no Altair representation; reported per component / inline body / response. A oneOf/anyOf body stays unmappable (skipped with --skip-unmappable); a union response surfaces as mixed.
  • additionalProperties (Phase 4d) — an open-ended map; reported per component / inline body / response. Declared properties still map; only the open-key capability is dropped. An explicit additionalProperties: false (closed object) is not warned.
  • operation + global security and components.securitySchemes — surfaced by design (Phase 5): Altair has no auth/middleware spec construct to generate into, so the requirement is reported (so you wire auth yourself) rather than silently dropped. This is a deliberate non-goal, like oneOf — not a pending gap.
  • servers, webhooks (3.1), callbacks, path-item $ref.

Surfaced as errors / skips (fail, or --skip-unmappable)

Section titled “Surfaced as errors / skips (fail, or --skip-unmappable)”
  • Remote (http(s)://) $ref, or a file $ref escaping the document directory — warned by the bundler and left unresolved, so the mapper surfaces them as unmappable (never fetched/read).
  • oneOf / anyOf in a request body; recursive $ref; a bare scalar body (incl. a normalized non-JSON body that turns out to be a scalar); an allOf whose subschema is not an object.
  • Remaining constraints: multipleOf, min/maxItems, uniqueItems (no Altair rule yet).
  • requestBody required flag; additionalProperties, const, discriminator, not, prefixItems, nullable; deprecated, default, example(s), title/description; response headers/links.
  • A bare required sibling on an allOf node (marking inherited properties required without re-declaring them) — required inside an allOf subschema is honored; a top-level required alongside allOf with no sibling properties is not yet applied (the flat field model has no slot for a required name without a property).

spec:emit-openapi emits operations, responses from output:, an application/json request body from body inputs, OpenAPI parameters for inputs tagged in: path|query|header|cookie (Phase 2), and schema constraints from validation rules — emailformat, min:3minLength, in:enum, regexpattern (Phase 3). security and servers are not emitted — there is no Altair spec construct that carries them (see the security note above).

See #214 for the phased plan (stop silent loss → parameters → validation fidelity → content & composition → security & misc). The epic is complete. Phases 1–3 (stop silent loss, parameters, validation), Phase 4 (4a non-JSON bodies, 4b allOf, 4c/4d oneOf/anyOf + additionalProperties, 4e external-$ref bundling), and Phase 5 (resource-naming fixes; path-param types; security surfaced by design) have all landed. Remaining items are deliberate non-goals (composition unions, auth middleware) or low-value constraints, each surfaced rather than dropped.