Repository navigation
fix(downgrader): drop an anyOf branch equal to the one before it in 3.0 - #148
Merged
Merged
Conversation
oRPC 1.x's Zod 4 converter writes `z.file().mime([...])` as an anyOf of strings with one media type each, which all become the same binary string in 3.0. NSwag and oapi-codegen then read the field as a generated class or a union instead of a file upload, and a nullable one stays a nested anyOf with the null branch. A branch equal to the one before it (same keys, same values, objects only by identity) is now removed, and a lone branch left takes the place of the anyOf when only annotations sit beside it, so the null pair merge applies. oneOf keeps its branches. Also notes in the README that oRPC 1.x's Valibot converter writes draft-07 by default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RsELoLWzSRAFMoTSS
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
dropEqualBranchesin the deferred union pass (packages/downgrader/src/v3.1-to-v3.0.ts:758) — drops ananyOfbranch shallow-equal to its immediate predecessor (isSameBranch,===per key, object values by identity), and hoists a lone remaining branch over theanyOfwhen the surrounding schema holds only annotations the branch lacks/repeats, or nothing beside a$refwhose 3.0 siblings are ignored.oneOfis untouched.- Deferred-pass gate (
:1357) —mergeEnums/mergeNullable/scalarOneOfnow run for ananyOfonly while more than one branch remains; a union reduced to a single branch is left as-is. This also avoidsmergeNullablewrongly addingnullable: trueto a lone non-null branch. - Tests — new case covering the oRPC file shapes, the nullable merge, non-neighbor branches,
$refhandling, annotation conflicts, non-record branches, andoneOf; three existing expectations adjusted where dedup now collapses branches. - README — new table row for the equal-branch behavior, plus the oRPC Valibot draft-07 note on the older-drafts limitation.
Verified locally: vitest run passes (284 tests, corpus and oRPC snapshots unchanged) and uncheck is clean (oxlint/oxfmt/tsc) on both changed source/test files. The equivalence reasoning holds — duplicate-anyOf removal is exact, and the hoist only proceeds when the surrounding keywords are non-assertive or preserved.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
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.

Requested by dinwwwh · project thread
Problem
oRPC 1.16.1 (npm
latest) with its Zod 4 converter writesz.file().mime(['application/pdf', 'image/png'])as one branch per media type:{ "anyOf": [{ "type": "string", "contentMediaType": "application/pdf" }, { "type": "string", "contentMediaType": "image/png" }] }Each branch becomes
{ type: "string", format: "binary" }, so 3.0 got ananyOfof identical branches, and.nullable()stayedanyOf [anyOf [binary, binary], null branch]. NSwag (C# and TS) then types the field as a generated class instead ofFileParameter, and oapi-codegen as a union wrapper instead ofopenapi_types.File. The flat{ type: "string", format: "binary" }(withnullable: true) gives proper uploads in both.Change
In the deferred union pass, before
mergeEnums/mergeNullable/scalarOneOf, ananyOfbranch equal to the one before it is removed. That is exact foranyOf. When one branch is left, it takes the place of theanyOfif the schema around it holds only annotations that the branch lacks or repeats (or nothing at all beside a$ref, whose siblings 3.0 ignores); otherwise it stays as a one-branchanyOf. A nullable file then merges as a pair, as before.oneOfis left alone, since dropping a branch there would widen it.Every oRPC 1.16.1 shape now comes out as a binary string: plain,
.describe(),.nullable(),.nullable().describe(),.optional(),.nullish(), andz.array(...)items.Two design choices, both from measurements:
JSON.stringifyof each branch, which blew up on dereferenced documents that share objects: a nullable object diamond took 2.2 s at depth 20 (2 ms on main) and hung at depth 64. Now depth 64 takes 3 ms.README: a row for the new behavior, plus the queued docs clause on the older-drafts limitation that oRPC 1.x's Valibot converter writes draft-07 unless given
{ target: 'draft-2020-12' }.Tests
oneOfof scalars), non-neighbors kept,$refalone vs. beside a description, a non-annotation sibling, conflicting vs. repeated annotations, non-record branches, andoneOfkept.anyOf [string, string]leaves the scalar-union "kept" list; a loosenedoneOftest gets a distinguishingminProperties; a loose tuple's items collapse to one schema.Verification
oneOfdisagrees on 373 of 3,000.Generated by Claude Code