Skip to content

fix(downgrader): give null its own anyOf branch for several types in 3.0 - #78

Merged
dinwwwh merged 1 commit into
mainfrom
claude/project-thread-ypzah5
Oct 24, 2026
Merged

dinwwwh merged 1 commit into
mainfrom
claude/project-thread-ypzah5

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 24, 2026

Copy link
Copy Markdown
Member

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 nullable inside an anyOf branch, so the generated types lost null. Now the branches leave nullable out and null gets its own branch, in the shape type: "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 and finishSchema's null-only block. The test and the README row for type with several types are updated.


Generated by Claude Code

`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
@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!

@macroscopeapp

macroscopeapp Bot commented Oct 24, 2026

Copy link
Copy Markdown

Approvability

Verdict: Approved at 6aa31a8

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.

@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-ypzah5 (6aa31a8) with main (a5d6ca6)

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

  • Null gets its own anyOf branch for multi-type schemas — convertType now drops nullable from each type branch and appends onlyNull() ({ type: 'string', nullable: true, enum: [null] }) when the type array contains "null", so generators that ignore nullable inside an anyOf branch keep null.
  • onlyNull() helper extracted — shared by the new branch and finishSchema's null-only block; the block's delete 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).

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

@dinwwwh
dinwwwh merged commit f9427cd into main Oct 24, 2026
11 checks passed
dinwwwh added a commit that referenced this pull request Oct 24, 2026
)

<!-- 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>
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