feat(downgrader): add @oasty/downgrader with 3.2-to-3.1 and 3.1-to-3.0 converters - #4
Open
dinwwwh wants to merge 7 commits into
Open
feat(downgrader): add @oasty/downgrader with 3.2-to-3.1 and 3.1-to-3.0 converters#4dinwwwh wants to merge 7 commits into
dinwwwh wants to merge 7 commits into
Conversation
- remove content-map entries whose mediaTypes reference cannot be inlined (external, unknown, or cyclic), with exact cycle detection instead of a depth cap, and resolve names without consulting the prototype chain - remove parameter-list and components references to removed querystring parameters instead of leaving them dangling - give synthesized anyOf array variants the items 3.0 requires - pass malformed $ref and allOf values through unchanged - keep tsc -b output out of the published dist (separate outDir) - document $defs/$anchor ref dangling and unknown-keyword limitations
Welcome to Codecov 🎉Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests. ℹ️ You can also turn on project coverage checks and project coverage reporting on Pull Request comment Thanks for integrating Codecov - We've got you covered ☂️ |
- drop additionalProperties together with patternProperties (its meaning would otherwise tighten onto pattern-matched keys); soften the loosening claim to positive positions and document not/oneOf polarity - remove parameters and headers whose entire content could not be inlined (3.1 requires exactly one entry there) - follow reference-alias chains when removing querystring parameters (fixpoint) and mutualTLS security schemes (cycle-guarded resolution) - omit security lists emptied by mutualTLS removal instead of leaving an explicit empty list that would make an operation public - drop empty enum, deduplicate required (3.0 structural constraints) - pass all-junk type arrays through instead of silently dropping them - narrow the never-throws claim to JSON-shaped (acyclic) input, correct the 3.2 dialect rationale, and de-vacuate the x- guard tests
deepClone now tracks visited containers in a WeakMap, preserving cycles and shared references in the clone instead of recursing forever, and the recursive converter choke points (schema, path item, media type) carry a cycle guard: a subtree that cycles back into an ancestor falls back to the cycle-preserving clone instead of converting without bound. The never-throws contract now genuinely covers dereferencer-style cyclic input; only pathologically deep nesting remains out of scope.
dinwwwh
force-pushed
the
claude/openapi-spec-downgrader-79cb74
branch
from
August 29, 2026 13:21
bb78763 to
78688e1
Compare
- correct the schema pass-through docs: the OAS 3.1 base-vocabulary meta-schema closes XML and Discriminator Objects to x- extras, so nodeType/defaultMapping are deliberately retained rather than 'legal' - pre-index components.parameters AND components.headers entries that conversion removes (querystring, or content fully uninlinable), iterated to a fixpoint over reference aliases, and remove references to them from parameter lists and header maps — no more dangling refs - synthesize required: true on path parameters (3.0 mandates it; 3.1 only structurally enforces it for schema-based ones) - add a prepack build guard so a publish can never ship an empty dist - corpus now validates every INPUT document too (3.1 and 3.2, the validator supports both), making valid-in -> valid-out an asserted property; style-defaults exclusion note corrected to the real blocker (x-comment in an Encoding Object, which the official 3.0 schema rejects for lack of an x- carve-out)
There was a problem hiding this comment.
Pull request overview
Adds @oasty/downgrader for staged OpenAPI 3.2 → 3.1 → 3.0 conversion.
Changes:
- Implements defensive document and schema converters.
- Adds comprehensive unit, corpus, validation, and snapshot tests.
- Configures package publishing, type-checking, and dependencies.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
package.json |
Adds the schema validator dependency. |
pnpm-lock.yaml |
Locks new package and validator dependencies. |
packages/downgrader/package.json |
Defines package metadata, exports, and scripts. |
packages/downgrader/tsconfig.json |
Configures package type-check output. |
packages/downgrader/README.md |
Documents conversion rules and limitations. |
packages/downgrader/src/index.ts |
Exports the public converter APIs. |
packages/downgrader/src/shared.ts |
Adds defensive cloning and traversal utilities. |
packages/downgrader/src/shared.test.ts |
Tests shared utilities and cycle handling. |
packages/downgrader/src/v3.1-to-v3.0.ts |
Implements 3.1-to-3.0 conversion. |
packages/downgrader/src/v3.1-to-v3.0.test.ts |
Tests 3.1-to-3.0 behavior. |
packages/downgrader/src/v3.2-to-v3.1.ts |
Implements 3.2-to-3.1 conversion. |
packages/downgrader/src/v3.2-to-v3.1.test.ts |
Tests 3.2-to-3.1 behavior. |
packages/downgrader/tests/corpus.test.ts |
Validates fixture corpus conversions. |
packages/downgrader/tests/e2e.test.ts |
Tests complete chained conversions. |
packages/downgrader/tests/__snapshots__/e2e.test.ts.snap |
Records expected converted documents. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
packages/downgrader/src/v3.1-to-v3.0.ts:207
constandenumare conjunctive JSON Schema constraints, so replacing an existing enum changes valid schemas. For example,{ const: 5, enum: [1, 2] }accepts nothing, but this emits{ enum: [5] }, which accepts5. Preserve the intersection: if the const is absent from the existing enum, emit the existing match-nothing 3.0 representation; otherwise narrowing to the const is safe.
// `const` is a single-value `enum`.
out.enum = [deepClone(schema.const)];
if (schema.const === null) {
out.nullable = true;
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+143
to
+148
| // `type: "null"` alone: 3.0's `nullable` needs a sibling `type`, so a | ||
| // single-value `enum` is the closest expressible form. | ||
| out.nullable = true; | ||
| if (!("enum" in schema) && !("const" in schema)) { | ||
| out.enum = [null]; | ||
| } |
When type: "null" is dropped for 3.0 (nullable needs a sibling type),
sibling enum/const constraints that the type used to filter became
reachable: {type: "null", enum: ["a", null]} started accepting "a"
and {type: "null", const: 7} started accepting 7 although the source
accepted null and nothing respectively. The null-only branch now
intersects: an enum containing null collapses to [null], and a sibling
excluding null emits a match-nothing not: {} — which also neutralizes
the enum convertConst writes. Types that survive keep enforcing the
intersection themselves, so no other branch changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the @oasty/downgrader package: it downgrades OpenAPI documents one minor version at a time (3.2 → 3.1 and 3.1 → 3.0), for whole documents or standalone Schema Objects. There is intentionally no direct 3.2 → 3.0 converter — the two steps compose. Converters never throw and never mutate their input: malformed parts pass through unchanged, existing
x-extensions and unknown keys always survive, and unsupported constructs are converted where a real equivalent exists and removed otherwise — no inventedx-keys.Conversion behavior
components.mediaTypesreferences are inlined (entries with external/unknown/cyclic targets are removed — 3.1 content maps cannot hold references), streamingitemSchemabecomes an array schema, responsesummaryand exampledataValue/serializedValuepromote into their 3.1 slots when free;$self, server names, tag metadata, QUERY/additional operations, and querystring parameters (including references to them) are removed. Schema Objects pass through as-is — 3.2 keeps the 3.1 dialect and JSON Schema tolerates extra keywords.typearrays becomenullable,constbecomes a single-valueenum, numeric exclusive bounds become bound + flag with the tighter bound winning, boolean schemas and$ref-with-siblings get their 3.0 forms, and 3.0-required fields (paths, operationresponses, arrayitems) are synthesized so output validates.Testing
items) or documented.Notes for review
descriptionfield (fix(types): add description field to 3.2 MediaTypeObject #3).tsc -btypecheck artifacts go to a separate gitignored directory.