Skip to content

fix(downgrader): keep a nullable anyOf that a $ref points into in 3.1 to 3.0 - #92

Merged
dinwwwh merged 2 commits into
mainfrom
claude/project-thread-gd6r1r
Oct 24, 2026
Merged

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

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Requested by dinwwwh · project thread

Fixes a regression from #87/#88. The nullable merge removes an anyOf of a branch and null (or #88 turns it into oneOf), but a $ref such as #/components/schemas/User/properties/home/anyOf/0 was kept as written and dangled, making the 3.0 document invalid.

Real producers of such refs: zod-to-json-schema 3.25 with its default $refStrategy: "root" (z.object({ home: Addr.nullable(), work: Addr.nullable() }) gives work: { anyOf: [{ $ref: "#/properties/home/anyOf/0" }, { type: "null" }] }, and a recursive Node.nullable() refs #/anyOf/0), and @apidevtools/swagger-parser bundle() with an external schema used nullable twice.

Changes

  • convertSchemaRef marks, for a $ref kept as written, each input schema whose anyOf the $ref passes through. The deferred nullable merge skips those, so the pair stays an anyOf and the $ref still resolves. The marks are cleared per call. No copies are made, so a recursive $ref can't create a cycle.
  • The first commit inlined such $refs instead, but a recursive one then reused the in-progress conversion of an ancestor, so the output held a JS cycle and JSON.stringify threw. The second commit replaces that.
  • Tests: the zod-to-json-schema pair keeps home's anyOf and work's $ref, and a recursive $ref into #/anyOf/0 keeps the anyOf and serializes. Both fail on main.
  • README limitations: such a pair stays an anyOf; $refs into a keyword 3.0 removes (prefixItems/0, propertyNames) or into a oneOf that becomes anyOf still dangle (pre-existing, rarer).

pnpm run check and all 258 tests pass; corpus snapshots are unchanged. A spec-level check of the self-referencing oneOf, the recursive anyOf, and the nullable pair gives valid 3.0 with no dangling refs.

🤖 Generated with Claude Code

https://claude.ai/code/session_013DHN6pxmaQMB2i4Rv3Kx35

The nullable merge removes an `anyOf` of a branch and null, and a `oneOf`
can become `anyOf`, so a `$ref` such as `#/.../home/anyOf/0` (written by
zod-to-json-schema's default root `$refStrategy` and by swagger-parser
`bundle()`) dangled in 3.0. Such refs are now replaced by their target,
like refs into `$defs`. Also lists refs into keywords 3.0 removes as a
README limitation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013DHN6pxmaQMB2i4Rv3Kx35
@dinwwwh dinwwwh self-assigned this Oct 24, 2026
@codecov

codecov Bot commented Oct 24, 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 24, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 21 untouched benchmarks


Comparing claude/project-thread-gd6r1r (4cabe65) with main (c716716)

Open in CodSpeed

@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

Reviewed the 3-file, single-commit fix for dangling $refs into anyOf/oneOf branches during 3.1→3.0 conversion.

  • Inlining heuristic — isInlined now treats refs containing /anyOf/ or /oneOf/ as inline targets, so refs into those branches are converted to their targets instead of being left dangling when 3.0 merges or renames those keywords.
  • Regression coverage — the new test covers a Zod-style nullable anyOf whose second branch refs the first, plus a $ref into a surviving oneOf; verified it fails without the source change.
  • Documentation — README now describes the new inlining behavior and the remaining limitation for refs into removed keywords such as prefixItems/0 and propertyNames.

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

… to 3.0

Inlining such `$ref`s, as the previous commit did, reused the in-progress
conversion of an ancestor for a recursive `$ref`, so the 3.0 output held a
cycle and JSON.stringify threw. Instead, a `$ref` kept as written marks
each schema whose `anyOf` it passes through, and the nullable merge (and
its `oneOf` rename) skips those, so the `$ref` still resolves. `$ref`s into
a `oneOf` that becomes `anyOf`, or into a removed keyword, are listed as a
README limitation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013DHN6pxmaQMB2i4Rv3Kx35
@dinwwwh dinwwwh changed the title fix(downgrader): inline $refs into an anyOf or oneOf in 3.1 to 3.0 fix(downgrader): keep a nullable anyOf that a $ref points into in 3.1 to 3.0 Oct 24, 2026
@dinwwwh
dinwwwh merged commit d14fb04 into main Oct 24, 2026
10 checks passed
dinwwwh added a commit that referenced this pull request Oct 24, 2026
…rgets (#95)

<!-- claude-projects-attribution: {"github_login":"dinwwwh"} -->
_Requested by **dinwwwh** · [project
thread](https://claude.ai/code/project/chan_01UcKMz91tMarPV1kWLVgLYE?thread=cmsg_01UcKMz91tMarPV1kWLVgLYE5YzkoJZXQSvTxTCyxe76P9)_

## Problem

Since #92, branch coverage is 99.78%. In `convertSchemaRef` (3.1 to
3.0), `if (isRecord(holder))` is only false for a kept `$ref` whose part
before `/anyOf/` doesn't resolve, which no test hits.

## Fix

`anyOfTargets` becomes a `Set<unknown>` and the resolved value is added
as is. The set is only queried with input schema objects in
`downgradeV30`, so an unresolved value in it changes nothing. +2/−5.

## Checks

- `pnpm run check` passes
- 262 tests pass
- Coverage is 100% for statements, branches, functions and lines
- Output is byte-identical on 66 real documents

---
_Generated by [Claude
Code](https://claude.ai/code/session_01YNzPR6fb2NHVQPYKsD2eta)_

Co-authored-by: Claude <noreply@anthropic.com>
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