Repository navigation
fix(downgrader): give null its own anyOf branch for several types in 3.0 - #78
Conversation
`type: ["string", "number", "null"]`, as Zod writes for a nullable union of primitives, became an `anyOf` whose branches each carried `nullable: true`. openapi-generator, openapi-zod-client and datamodel-codegen ignore `nullable` inside an `anyOf` branch, so null was lost. The branches now leave it out, and null gets its own branch in the shape `type: "null"` already uses. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfcW36YwHrPJoeGRCsiPyo
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This focused downgrader fix preserves nullability by emitting a dedicated null branch instead of relying on nullable inside other anyOf branches. Its runtime scope is limited to multi-type schemas containing null, with aligned documentation and regression coverage. You can add or adjust custom eligibility rules. Learn more. |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Null gets its own
anyOfbranch for multi-type schemas —convertTypenow dropsnullablefrom each type branch and appendsonlyNull()({ type: 'string', nullable: true, enum: [null] }) when the type array contains"null", so generators that ignorenullableinside ananyOfbranch keep null. onlyNull()helper extracted — shared by the new branch andfinishSchema's null-only block; the block'sdelete out.enum; Object.assign(out, onlyNull())preserves the previous key order (type,nullable,enum).- Test and README updated — the
['array','string','null']expectation now asserts the 3-branch shape, and the "several types" README row documents the null branch.
The reshaping is semantically equivalent to the prior output (both accept rest ∪ {null}), so there is no behavior regression. I ran the downgrader suite (197 passed, no snapshot churn) and uncheck over the changed files (oxlint, oxfmt, tsc all pass).
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
) <!-- claude-projects-attribution: {"github_login":"dinwwwh"} --> _Requested by **dinwwwh** · [project thread](https://claude.ai/code/project/chan_01UcKMz91tMarPV1kWLVgLYE?thread=cmsg_01UcKMz91tMarPV1kWLVgLYEC8ii77bvJTYMXNpA7YYZXA)_ Adds one line to the 3.1 → 3.0 Schema limitations in `packages/downgrader/README.md`, next to the OpenAPI Generator line from #81. Kiota 1.36 reads the `type: "string"` of the `"null"` branch (`{ type: "string", nullable: true, enum: [null] }`, from #72 and #78) as a real type. So from the 3.0 output, `z.array(User.nullable())` items and a nullable union of several types come out as strings. Kiota also reads 3.1, which types them correctly. The null-branch shape stays as is, since every other shape tried breaks some other tool (an untyped or bare null shape fixes Kiota but breaks Ajv or hey-api). `pnpm run check` passes. --- _Generated by [Claude Code](https://claude.ai/code/session_01ABAx6Ud1HPRYdomPGUkrKG)_ --------- Co-authored-by: Claude <noreply@anthropic.com>

Requested by dinwwwh · project thread
Zod writes
z.union([z.string(), z.number()]).nullable()as{ "type": ["string", "number", "null"] }. In 3.0 that became:{ "anyOf": [{ "type": "string", "nullable": true }, { "type": "number", "nullable": true }] }openapi-generator, openapi-zod-client and datamodel-codegen ignore
nullableinside ananyOfbranch, so the generated types lost null. Now the branches leavenullableout and null gets its own branch, in the shapetype: "null"already uses (#72):{ "anyOf": [ { "type": "string" }, { "type": "number" }, { "type": "string", "nullable": true, "enum": [null] } ] }A small
onlyNull()helper writes that shape for both this branch andfinishSchema's null-only block. The test and the README row fortypewith several types are updated.Generated by Claude Code