Repository navigation
fix(downgrader): write */* as the contentType of an Encoding Object that sets style, explode, or allowReserved - #323
Conversation
… 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
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Merging this PR will not alter performance1 warning
Comparing Footnotes
|
There was a problem hiding this comment.
✅ 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.
finishEncodingon Encoding Objects —MEDIA_TYPE_FIELDS.encodingnow routes each encoding throughconvertObject(..., ENCODING_FIELDS, finishEncoding), which setsout.contentType = '*/*'iff the source explicitly setsstyle,explode, orallowReserved. This matches 3.1's "value ofcontentType(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-urlencodedmedia types, so*/*is the loosest correct representation of the unconstrained part.- Test coverage — the new test exercises
explode,style, andallowReserved: false(each beside or without an explicitcontentType) 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
multipartpart'sapplication/octet-streamdefault).
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.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Requested by dinwwwh · project thread
Summary
In 3.1, when an Encoding Object sets
style,explode, orallowReserved, itscontentType(explicit or default) is ignored, and that holds formultipart/form-datatoo. 3.0 ignores those three inmultipartand appliescontentTypeinstead. So amultipartpart 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
finishhook on Encoding Objects: when any of the three is set,contentTypebecomes*/*, which accepts any part type. Everything else passes through as before. The README gains one table row, and the limitation about 3.0 ignoringstyle/explode/allowReservedinmultipartis narrowed to the one gap left (the untyped part'sapplication/octet-streamdefault).Tradeoffs
contentTypeto 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 writestyle/explode/allowReservedin encodings, so its output is unchanged.Test plan
style,explode, andallowReservedeach, and an Encoding Object without them left as is.vitest run packages/downgrader(300 passed) andpnpm checkpass locally.Generated by Claude Code