Skip to content

fix(downgrader): make a nullable allOf a oneOf when any member rejects null - #202

Merged
dinwwwh merged 2 commits into
mainfrom
claude/project-thread-qqtv6k
Oct 29, 2026
Merged

dinwwwh merged 2 commits into
mainfrom
claude/project-thread-qqtv6k

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 29, 2026

Copy link
Copy Markdown
Member

Requested by dinwwwh · project thread

What

mergeNullable makes an anyOf null pair a oneOf when the other branch rejects null (#126). For an allOf branch it looked only at allOf[0], and only when that was a $ref. So these stayed an anyOf in 3.0:

  • oRPC's z.intersection(B, T).nullable() with T registered, which writes anyOf [{ allOf [B, $ref T] }, { type: "null" }] (the $ref second).
  • an intersection of two inline objects, anyOf [{ allOf [object, object] }, { type: "null" }].

NSwag reads that anyOf as an empty class (checked with NSwag 14 C#: the anyOf gives an empty V, the oneOf gives V : T holding B), which is the reason #126 makes the pair a oneOf.

Fix

An allOf rejects null when any member does, so every member is checked, following a member's $ref. That is exact: null fails the allOf, so it matches only the null branch. An allOf whose members all admit null stays an anyOf.

The README row for the anyOf null pair now says the pair also becomes a oneOf for "an allOf with such a member in any place, as Zod writes a nullable intersection".

Tests

New test: the $ref-second intersection becomes a oneOf, two inline objects become a oneOf, and an allOf of an untyped member and a $ref to a schema that admits null stays an anyOf. The test fails on main. pnpm run check and pnpm test pass (363 tests), and coverage stays at 100%.


Generated by Claude Code

…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
@dinwwwh dinwwwh self-assigned this Oct 29, 2026
@codecov

codecov Bot commented Oct 29, 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 Oct 29, 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-qqtv6k (ff1557c) with main (8e11ec5)

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 critical issues — one minor test gap inline.

Reviewed changes

  • mergeNullable allOf null check — 3.1→3.0 now makes an anyOf null pair a oneOf when any member of the non-null allOf branch rejects null, following each member's $ref, instead of only checking allOf[0] when it was a $ref.
  • README — the anyOf null-pair row documents the new allOf case.
  • Test — covers a $ref-second intersection, two inline objects, and an all-admit-allOf that stays an anyOf.

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).

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread packages/downgrader/tests/v3.1-to-v3.0.test.ts Outdated

@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

  • Mixed-allOf test coverage — ff1557c reworks the allOf test's owner helper to take both the first member and the $ref target, 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.

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

@dinwwwh
dinwwwh merged commit 8a0d645 into main Oct 29, 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