Repository navigation
fix(downgrader): make a nullable allOf a oneOf when any member rejects null - #202
Conversation
…s null mergeNullable read only the first `allOf` member, so oRPC's `z.intersection(B, T).nullable()` (with the `$ref` second) and an intersection of two inline objects stayed an `anyOf` pair in 3.0, which NSwag reads as an empty class. An `allOf` rejects null when any member does, so check every member, following a member's `$ref`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RdCF4usMbdkSRvsKh
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 critical issues — one minor test gap inline.
Reviewed changes
mergeNullableallOfnull check — 3.1→3.0 now makes ananyOfnull pair aoneOfwhen any member of the non-nullallOfbranch rejects null, following each member's$ref, instead of only checkingallOf[0]when it was a$ref.- README — the
anyOfnull-pair row documents the newallOfcase. - Test — covers a
$ref-second intersection, two inline objects, and an all-admit-allOfthat stays ananyOf.
The transformation is sound: an allOf rejects null when any member does, and the null branch (isNullBranch) matches only null, so the two branches stay disjoint and anyOf→oneOf is exact. I confirmed the new test fails on the base file (it returns anyOf) and passes on the head, and the full downgrader suite passes (286 tests).
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RdCF4usMbdkSRvsKh
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Mixed-
allOftest coverage —ff1557creworks theallOftest'sownerhelper to take both the first member and the$reftarget, so one member rejecting null while the other admits it is now asserted in both positions. This pins the "any member rejects null" semantics that the prior review flagged as unisolated.
I verified the gap is genuinely closed: replacing .some with .every in mergeNullable now fails the new test at tests/v3.1-to-v3.0.test.ts:1092 (received anyOf, expected oneOf), where it previously left the whole suite green. The source hunk is unchanged from the previously reviewed commit, and the transformation remains sound — an allOf rejects null when any member does, and the isNullBranch branch matches only null, so the two oneOf branches stay disjoint.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Requested by dinwwwh · project thread
What
mergeNullablemakes ananyOfnull pair aoneOfwhen the other branch rejects null (#126). For anallOfbranch it looked only atallOf[0], and only when that was a$ref. So these stayed ananyOfin 3.0:z.intersection(B, T).nullable()withTregistered, which writesanyOf [{ allOf [B, $ref T] }, { type: "null" }](the$refsecond).anyOf [{ allOf [object, object] }, { type: "null" }].NSwag reads that
anyOfas an empty class (checked with NSwag 14 C#: theanyOfgives an emptyV, theoneOfgivesV : TholdingB), which is the reason #126 makes the pair aoneOf.Fix
An
allOfrejects null when any member does, so every member is checked, following a member's$ref. That is exact: null fails theallOf, so it matches only the null branch. AnallOfwhose members all admit null stays ananyOf.The README row for the
anyOfnull pair now says the pair also becomes aoneOffor "anallOfwith such a member in any place, as Zod writes a nullable intersection".Tests
New test: the
$ref-second intersection becomes aoneOf, two inline objects become aoneOf, and anallOfof an untyped member and a$refto a schema that admits null stays ananyOf. The test fails on main.pnpm run checkandpnpm testpass (363 tests), and coverage stays at 100%.Generated by Claude Code