Repository navigation
fix(downgrader): merge a oneOf with null, as typia and Litestar write it, in 3.1 to 3.0 - #105
Conversation
… it, in 3.1 to 3.0
`oneOf: [X, { type: "null" }]` now becomes X with `nullable: true` under
the same conditions as the `anyOf` pair (#87), so oapi-codegen and other
generators read a nullable value instead of a union. Only a pair merges,
since a `oneOf` with null twice rejects null, and a `$ref` into the
`oneOf` keeps it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DRHCq36GvkQQjq7BJHMsZ
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 critical issues — one minor documentation follow-up.
Reviewed changes
- Merge a null
oneOfpair —finishSchemanow queues a two-branchout.oneOfalongside the existinganyOfcases, andmergeNullable(out, root, key)collapsesoneOf: [X, { type: "null" }]intoXwithnullable: trueunder the same guards asanyOf(no$ref/enum/applicator/nullableon the branch, no key shared with the schema around it). $refinto aoneOfkeeps it —convertSchemaRefnow scans/oneOf/as well as/anyOf/segments, sounionTargetsprotects aoneOfa kept$refpoints into.- Rename —
nullableAnyOfs/anyOfTargetsbecomenullableUnions/unionTargetscarrying akey: 'anyOf' | 'oneOf'tag; theanyOf → oneOfrewrite branch is gated tokey === 'anyOf', since aoneOfinput is already the desired shape. - Test + README row — a new
itcovers the merge, the null-twice case,type: [...]/enum/$refbranches staying, and a$refinto/oneOf/keeping it; one behavior-table row documents the pair.
Verified locally: vitest run 269/269 pass and uncheck on the three touched files passes (oxlint, oxfmt, tsc). I traced the merge equivalence (type with a non-null string rejects null, so the pair equals X + nullable) and found no correctness gap.
ℹ️ README limitations still name only anyOf for the $ref-pointed-into union
The Limitations list explains why a null union that a $ref points into is left as written, but it names anyOf alone. This PR extends exactly that behavior to oneOf (with a test asserting it), so a reader no longer learns the whole rule.
Technical details
# README limitation omits the new `oneOf` case
## Affected sites
- `packages/downgrader/README.md:249` — "An `anyOf` of `{ type: "null" }` and a branch that a `$ref` points into stays as written, so the `$ref` still resolves, and code generators read it as a union." Only names `anyOf`.
## Required outcome
- The sentence reflects that the same preservation now applies to a `oneOf` of `{ type: "null" }` and one other branch (e.g. widen to "an `anyOf` or `oneOf`" or add a oneOf example).
## Notes
- Purely a docs completeness nit, non-blocking. The behavior itself is correct and covered by the new test (`packages/downgrader/tests/v3.1-to-v3.0.test.ts:596`).deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DRHCq36GvkQQjq7BJHMsZ
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Widened the null-union
$reflimitation — commit65348f7updates the README Limitations line to nameoneOfalongsideanyOffor a null union that a kept$refpoints into, matching the behaviorconvertSchemaRef/unionTargetsalready implement. This addresses the sole finding from the prior review; no source or test changes since6afbd7b.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Requested by dinwwwh · project thread
Since #87, 3.1 to 3.0 merges
anyOf: [X, { type: "null" }]into X withnullable: true. typia and Litestar write the same nullable value as aoneOf(so do the Figma, Phoenix and Lago docs):{ "oneOf": [{ "type": "object", "properties": { "a": { "type": "string" } } }, { "type": "null" }] }That pair stayed a
oneOfin 3.0, and oapi-codegen turns each one into a union wrapper (17 unions vs 9 merged on a Litestar app) instead of a nullable field.Now a
oneOfof two branches is queued beside theanyOfs and merges under the same conditions:oneOfwith{ type: "null" }twice rejects null, so it stays.$ref,enum, an applicator ornullable, or one that shares a keyword with the schema around it, stays, as foranyOf. TheanyOftooneOfrewrite for those does not apply, since it is already aoneOf.$refkept as written that points into/oneOf/keeps theoneOf, as fix(downgrader): keep a nullable anyOf that a $ref points into in 3.1 to 3.0 #92 does for/anyOf/.One README row and one test.
pnpm run checkand the tests pass with 100% coverage. A gc'd, shuffled wall-time bench with twin builds shows no change (api 100 resources 19.8 ms, corpus 1.69 ms, path item chain 200 1.07 ms, reference diamond 64 0.55 ms on both main and this branch).Generated by Claude Code