Skip to content

Support OpenAPI 3.1 nullable union - #77

Merged
owjs3901 merged 1 commit into
mainfrom
owjs3901/fix-openapi31-type-arrays
Aug 24, 2026
Merged

Support OpenAPI 3.1 nullable union#77
owjs3901 merged 1 commit into
mainfrom
owjs3901/fix-openapi31-type-arrays

Conversation

@owjs3901

Copy link
Copy Markdown
Contributor

Problem

OpenAPI 3.1 expresses a nullable $ref as a union with the JSON Schema null type:

{ "anyOf": [{ "$ref": "#/components/schemas/Item" }, { "type": "null" }] }

The generator treated anyOf / oneOf as a plain union, so { "type": "null" } was
converted to unknown and the $ref lost its component identity:

position before after
request body ({ id?: string } | unknown) DevupObject<'request', 'openapi.json'>['Item']
response property (DevupObject<...>['Item'] | unknown) DevupObject<'response', 'openapi.json'>['Item']
zod schema z.union([...]) z.lazy(() => _Item)
zod path mapping (dropped) createItem: openapi_json_request_Item

(T | unknown) collapses to unknown in TypeScript, which is the reported
TS2322: Type 'unknown' is not assignable to .... On the zod side the result became a
ZodUnion, which has no .unwrap().

Fix

anyOf: [S, { type: "null" }] is 3.1's nullable notation, not a union. Two helpers in
openapi-utils.ts normalize it to the OpenAPI 3.0 equivalent { ...S, nullable: true }
before any union handling, so the existing nullable machinery applies unchanged:

  • isNullTypeSchema() — detects a { "type": "null" } member
  • splitNullableUnion() — splits union members from the nullability they contribute
  • normalizeNullableUnion() — collapses a single-member null union to the 3.0 shape,
    preserving sibling keywords (description, default) and returning the input
    unchanged when there is nothing to collapse

Applied on all three paths:

  • generate-schema.tsgetTypeFromSchema() union branch
  • generate-zod.tsschemaToZod(), schemaToZodType(), and the request-body schema
    name lookup that fed postPathSchemas
  • generate-interface.ts — response content and request-body branches, so the component
    reference shortcut and the raw-multipart check see the normalized schema

Unions that keep two or more members stay unions and now additionally reflect a removed
null member, so anyOf: [string, number, null] generates (string | number | null)
instead of (string | number | unknown).

Not regressed

  • Genuine unions are untouched: anyOf: [{type:"string"},{type:"number"}] still generates
    (string | number) / z.union([...]) / z.ZodUnion<...>.
  • OpenAPI 3.0 nullable: true input keeps working.
  • type: ["string","null"] arrays were already handled by getPrimaryType() /
    isNullableSchema() and are unchanged.

Tests

New nullable-union.test.ts asserts that every 3.1 nullable form generates a
byte-identical result to its 3.0 counterpart across generateInterface,
generateZodSchemas, and generateZodTypeDeclarations, in both a request-body and a
component-property position. Verified TDD: 14 of the new tests fail without the fix and
all 31 pass with it, while the genuine-union guards pass in both states.

bun test 1148 pass / 0 fail, 100% line and function coverage on all four changed files,
bun run lint and bun run build clean.

@github-actions

Copy link
Copy Markdown

Changepacks

@devup-api/generator@0.1.25 → 0.1.26 - packages/generator/package.json

Patch

  • Support OpenAPI 3.1 nullable union

@devup-api/next-plugin@0.1.14 → 0.1.15 - packages/next-plugin/package.json

Patch

  • Auto-update: depends on '@devup-api/generator' via a local workspace dependency

@devup-api/rsbuild-plugin@0.1.14 → 0.1.15 - packages/rsbuild-plugin/package.json

Patch

  • Auto-update: depends on '@devup-api/generator' via a local workspace dependency

@devup-api/vite-plugin@0.1.14 → 0.1.15 - packages/vite-plugin/package.json

Patch

  • Auto-update: depends on '@devup-api/generator' via a local workspace dependency

@devup-api/webpack-plugin@0.1.14 → 0.1.15 - packages/webpack-plugin/package.json

Patch

  • Auto-update: depends on '@devup-api/generator' via a local workspace dependency

@owjs3901
owjs3901 merged commit d0c5cc3 into main Aug 24, 2026
2 checks passed
@owjs3901
owjs3901 deleted the owjs3901/fix-openapi31-type-arrays branch August 24, 2026 17:24
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.

1 participant