Skip to content

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
mainfrom
claude/openapi-spec-downgrader-79cb74
Open

feat(downgrader): add @oasty/downgrader with 3.2-to-3.1 and 3.1-to-3.0 converters#4
dinwwwh wants to merge 7 commits into
mainfrom
claude/openapi-spec-downgrader-79cb74

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Aug 29, 2026

Copy link
Copy Markdown
Member

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 invented x- keys.

Conversion behavior

  • 3.2 → 3.1: components.mediaTypes references are inlined (entries with external/unknown/cyclic targets are removed — 3.1 content maps cannot hold references), streaming itemSchema becomes an array schema, response summary and example dataValue/serializedValue promote 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.
  • 3.1 → 3.0: type arrays become nullable, const becomes a single-value enum, 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, operation responses, array items) are synthesized so output validates.
  • The README documents every rule and the known lossy limitations.

Testing

  • 318 tests, 100% coverage (statements, branches, functions, lines); the corpus suites validate every input document and every converted hop against the official OpenAPI JSON Schemas, so valid-in -> valid-out is an asserted property.
  • E2E and corpus suites validate every converted document against the official OpenAPI JSON Schemas via @seriousme/openapi-schema-validator: ~74 real fixture documents (OAI examples and schema-test corpus) are converted through each hop and must come out valid; petstore passes through 3.1 → 3.0 byte-identical apart from the version stamp.
  • A multi-agent adversarial review ran against the final code; all confirmed findings are fixed (unresolvable content references now removed with exact cycle detection, no prototype-chain lookups, dangling querystring parameter references removed, synthesized array variants carry items) or documented.

Notes for review

- 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
@codecov

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

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
dinwwwh force-pushed the claude/openapi-spec-downgrader-79cb74 branch from bb78763 to 78688e1 Compare August 29, 2026 13:21
- 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)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • const and enum are 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 accepts 5. 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants