Skip to content

fix(downgrader): write */* as the contentType of an Encoding Object that sets style, explode, or allowReserved - #323

Merged
dinwwwh merged 25 commits into
mainfrom
claude/project-thread-h3q238
Nov 1, 2026
Merged

dinwwwh merged 25 commits into
mainfrom
claude/project-thread-h3q238

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Nov 1, 2026

Copy link
Copy Markdown
Member

Requested by dinwwwh · project thread

Summary

In 3.1, when an Encoding Object sets style, explode, or allowReserved, its contentType (explicit or default) is ignored, and that holds for multipart/form-data too. 3.0 ignores those three in multipart and applies contentType instead. So a multipart part that 3.1 accepts in any type was held to one type after the downgrade, which makes the 3.0 document stricter.

The fix is a finish hook on Encoding Objects: when any of the three is set, contentType becomes */*, which accepts any part type. Everything else passes through as before. The README gains one table row, and the limitation about 3.0 ignoring style/explode/allowReserved in multipart is narrowed to the one gap left (the untyped part's application/octet-stream default).

Tradeoffs

  • A code generator that reads contentType to pick a part's serialization sees */* instead of, say, application/json. Clients that relied on it will send the part as raw bytes or text. That loses a hint, but no value 3.1 accepts is rejected. oRPC does not write style/explode/allowReserved in encodings, so its output is unchanged.

Test plan

  • New test covering style, explode, and allowReserved each, and an Encoding Object without them left as is.
  • vitest run packages/downgrader (300 passed) and pnpm check pass locally.

Generated by Claude Code

claude added 25 commits November 1, 2026 05:44
… no oneOf branch apart, and takes null

Where several $refs enter the same cycle, the first copy made is reused, so
a later one can hold a $ref that was cut to {} while another target was
being inlined. isDisjoint and the nullable pair merge read such a $ref in
the input, saw its target's type or tag, and kept a oneOf that the {} then
let a value match twice. The holders of cut $refs are now recorded, and
follow() and throughRef() read them as keeping nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7NmVyQoYtVBNPuE4uV
…operties beside other properties, out of enum merging

A oneOf rejects a value two of its branches share, so mergeEnums keeps a
oneOf whose literals repeat one. It compared them serialized, which tells
equal objects with their keys in another order apart, so it merged such a
oneOf into an enum that took the shared value. A oneOf of object or array
literals now stays as it is. And an additionalProperties in a literal branch
counted as repeating the one around it although that one sits beside other
properties, which it then let through, so it repeats only beside the same
properties.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7NmVyQoYtVBNPuE4uV
…side annotations

enumNames read the names at the end of the propertyNames $ref chain, past a
$ref with an enum, maxLength or other keyword beside it that narrows the
names further. The record then got a property for each name of the last
enum, allowing keys the input rejects, and counted as converted exactly, so a
not or oneOf over it stayed. The chain now stops at such a $ref, so the
record loses its key names and counts as loose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7NmVyQoYtVBNPuE4uV
…iction around an alias component or a cycle of objects

#309 converts the input again when a not or oneOf read a $ref's target
before that target lost a restriction around a cycle. It missed two cycles.
A component that is only a $ref, an alias, has no output of its own, so the
output recorded for it was undefined; it now records the output of the target
its chain of kept $refs ends at. And a dereferenced document closes a cycle
with the object itself rather than a $ref; convertObject now tells
convertSchema when it meets a schema again, and one that was not loose then
but is once converted makes the next pass read it as loose. First visits pay
nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7NmVyQoYtVBNPuE4uV
…ed-union $refs

The rerun recorded nothing for an alias whose chain passes through a $ref the
conversion inlines, such as one into $defs, or that is a $ref to a tagged union
beside only annotations, so a not over it around a cycle was kept over a target
that lost a restriction. sharedOutput now follows every $ref convertSchema
treats as an alias, and reads an inlined hop's output from its inlined copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7NmVyQoYtVBNPuE4uV
…form

A schema that loses a restriction around a cycle made the conversion run
again, so a not or oneOf that read it too early could drop or widen. Only
such a choice changes when more schemas read as loose, so a pass that kept
no not, and no oneOf without a loose branch, needs no other. A document with
a recursive schema that loses a restriction and no not or oneOf now converts
in one pass, about half the time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7NmVyQoYtVBNPuE4uV
…y property as loose

OAS 3.0 applies a required readOnly property to responses only and a
required writeOnly property to requests only, while 3.1 requires it both
ways. The 3.0 output so loses that restriction, so a not or oneOf over
such an object must not keep it, and the property tells no oneOf branch
apart. The property is read through a chain of $refs too, as 3.0 reads
each target in its place.

Such a schema in a cycle, as a recursive DTO with a required readOnly id,
makes a document that keeps a not or oneOf convert twice, which the README
performance note now says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7NmVyQoYtVBNPuE4uV
… or a set per name

CodSpeed read the order schema bench 19% slower with the check. It now loops
over `required` once per schema, reads each property's fields directly, and
makes a visited set only past the first `$ref`. Instructions per conversion of
that schema drop from 4% over main to 2% under callgrind with CodSpeed's V8
flags. A cycle of `$ref`s, which never reaches a schema, is tested too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7NmVyQoYtVBNPuE4uV
Marking the schema itself loose lets a $ref to it, and every object around
it, read as loose through the existing checks, so no walk over required
names is needed. Only the tag check still reads through $refs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7NmVyQoYtVBNPuE4uV
…stead of converting again

A $ref or object cycle that reaches a schema before its conversion is done
cannot know yet what that schema loses, so it now counts as loose. This
replaces the second pass (#309, #316) and its skip (#317) with two checks,
at the cost of a recursive schema always counting as loose: a not over it is
removed, and a oneOf with it becomes anyOf unless its branches are disjoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7NmVyQoYtVBNPuE4uV
…ties with its target

Checking which items or additionalProperties the conversion replaces needed
a walk over the pointer and a lookup of each holder. Replacing every $ref
into either keeps the same values accepted, with one regex test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7NmVyQoYtVBNPuE4uV
A target that is a $ref again now holds no names, so the record loses its
key names, as it does for any other propertyNames it cannot read. This
replaces the walk along a chain of annotated $refs and its visited set,
which every schema allocated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7NmVyQoYtVBNPuE4uV
find() percent-decodes a $ref, but the checks for $defs, the removed parts,
and items or additionalProperties read it as written. So a $ref such as
#/components/schemas/Pair/%69tems was kept, and read the looser schema the
conversion puts there, which a not over it reads as stricter. Any $ref with
a % is now replaced by its target, which needs no decoding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7NmVyQoYtVBNPuE4uV
… target

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7NmVyQoYtVBNPuE4uV
…hat sets style, explode, or allowReserved

3.1 ignores contentType, also its default, where style, explode, or
allowReserved is set, while 3.0 ignores those in multipart and applies
contentType, so a part 3.1 accepts in any type was held to one type in 3.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7NmVyQoYtVBNPuE4uV
@dinwwwh dinwwwh self-assigned this Nov 1, 2026
@codecov

codecov Bot commented Nov 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Nov 1, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 1 benchmark is dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 29 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing claude/project-thread-h3q238 (98c8199) with main (b441d6a)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@pullfrog pullfrog Bot 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.

✅ No new issues found.

Reviewed changes — this run reviewed the 3.1→3.0 Encoding Object change in 98c8199, verified against the OAS 3.1.0 and 3.0.3 Encoding Object text.

  • finishEncoding on Encoding Objects — MEDIA_TYPE_FIELDS.encoding now routes each encoding through convertObject(..., ENCODING_FIELDS, finishEncoding), which sets out.contentType = '*/*' iff the source explicitly sets style, explode, or allowReserved. This matches 3.1's "value of contentType (implicit or explicit) SHALL be ignored" rule for all three fields, and 3.0's handling of them as ignored for non-x-www-form-urlencoded media types, so */* is the loosest correct representation of the unconstrained part.
  • Test coverage — the new test exercises explode, style, and allowReserved: false (each beside or without an explicit contentType) plus a control part with none of the three, which passes through unchanged.
  • README — one table row is added, and the "Encoding Objects pass through" limitation is narrowed to the one remaining gap (an untyped multipart part's application/octet-stream default).

The finishEncoding presence test (!== undefined) correctly treats false as defined, plain assignment is the established idiom for constant literal keys in this file, and 3.2→3.1 needs no mirror fix since 3.2 carries the same contentType-ignored rule.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@dinwwwh
dinwwwh merged commit cf0459b into main Nov 1, 2026
11 checks passed
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