Repository navigation
fix(downgrader): read a tag property through a $ref when keeping a oneOf - #206
Conversation
A oneOf with a branch that loosens in 3.0 stays a oneOf only when the
branches are apart, which the check reads from each branch's const or enum
properties. A property written as a $ref to a one-value enum or const, as
Zod writes z.literal('a').meta({ id }) and the Entitle API each tag, held
nothing, so the union became an anyOf. The check now follows a $ref with
only annotations beside it, as it already does for a branch and its allOf
members.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VVaw7RoZZwQ34LdS1QdzFN
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
tagsOffollows a property$ref— when a tag property is a$refbeside only annotations (isAnnotatedRef), it is resolved viafollowso its target'sconst/enumvalues contribute to the disjointness check.followextracted to top level — the$ref-chain walker (with its cycle / in-progress-inline cut) moved out ofpartTagsverbatim and now takesctxexplicitly.- README row updated — the
oneOf-with-a-loosened-branch row notes theconst/enummay now come through a$refwith only annotations beside it. - New test — covers bare
$reftoconst/one-valueenum, an annotated$ref, and a chain through an alias (all keeponeOf), plus a$refbesidetype: ["string","null"]and one to a nullable enum (both stayanyOf).
The change is conservative: follow reads the final target's const/enum, which is always a superset of the effective values across a $ref chain, so it can only suppress a oneOf, never create one falsely. Enum/const are preserved by the 3.0 conversion and kept $refs still resolve in the output. Verified locally: the new test passes, all 288 downgrader tests pass, and pnpm run check is green.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Requested by dinwwwh · project thread
When a
oneOfbranch loses a restriction in 3.0, such as a tuple, theoneOfbecomes ananyOfunlessisDisjoint(#139) proves no value matches two branches. It reads each branch's tag property withtagsOf, which tookconst/enumonly from an inline property. A tag written as a$refto a one-valueenumorconstheld nothing, so the union became ananyOfalthough the tags tell the branches apart.partTagsalready followed a$reffor a branch and itsallOfmembers, just not for a property.Change
follow(the$refchain walker that was local topartTags, with its recursion-cut check) is now a top-level function.tagsOftakesctxand follows a property that is a$refwith only annotations beside it (isAnnotatedRef, as Zod writes a described$ref). A$refwith other siblings, such as atypenaming null that becomesnullable, is still not read.oneOfwith a loosened branch: theconst/enummay come through a$refwith only annotations beside it.Where it shows up
z.discriminatedUnion('kind', [...])whose tag isz.literal('a').meta({ id })orz.enum(['a']).meta({ id })giveskind: { $ref }. With one branch holding az.tuple, main writesanyOf; this keepsoneOf.ApprovalFlowStepResponseSchema.approvalEntities.itemsand three similar unions of 4 refs, each tagged by a$refto its own one-value enum, becomeoneOfagain. NSwag 14.7 then writes the first branch's class instead of an empty class.Hunt checks on the patched build: a 19,775-doc corpus diff changes only Entitle's 4 unions (8 sites), no throws; the Entitle output stays valid 3.0; the value oracle loses 0 of 22,108 values.
Tests
New test
reads the values of a property through a $ref beside only annotations(fails on main): bare$refto aconstand to a one-valueenum, an annotated$ref, and a chain through an alias keeponeOf; a$refbesidetype: ['string', 'null']and a$refto a nullable enum still giveanyOf. 365 tests pass, coverage stays 100%,pnpm run checkpasses.Generated by Claude Code