Skip to content

fix(zod): Zod v4 plugin does not respect nullable propertyΒ #2732

@mrjasonroy

Description

@mrjasonroy

Description

The Zod v4 plugin does not respect the nullable: true property in OpenAPI schemas. When a property is marked as nullable in the spec, the generated Zod schema should wrap the type with .nullable(), but currently it does not.

Current Behavior

Given an OpenAPI schema with nullable: true:

{
  "type": "object",
  "properties": {
    "description": {
      "type": "string",
      "nullable": true
    }
  }
}

The Zod plugin generates:

z.object({
  description: z.optional(z.string())
})

This fails validation when the API returns null because Zod v4's z.optional() only accepts undefined, not null.

Expected Behavior

The generated Zod schema should be:

z.object({
  description: z.optional(z.nullable(z.string()))
})

This allows both undefined (when field is omitted) and null (when field is explicitly null).

Steps to Reproduce

  1. Create an OpenAPI spec with a nullable property:
{
  "components": {
    "schemas": {
      "TestSchema": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string",
            "nullable": true
          }
        }
      }
    }
  }
}
  1. Generate Zod schemas using the plugin
  2. Try to parse data with null value:
const schema = z.object({
  description: z.optional(z.string()) // Generated
})

schema.parse({ description: null }) // ❌ Fails: Expected string, received null

Environment

  • @hey-api/openapi-ts: 0.84.4
  • Zod plugin version: v4
  • Zod: 3.x

Root Cause

The Zod v4 plugin's schemaToZodSchema function handles the optional flag (line 1162-1171) but does not check for schema.nullable. The nullable handling only exists for enum types (line 228-229, 274-282) but not for regular property types.

Proposed Solution

Add a nullable check in the schemaToZodSchema function before the optional check:

if (schema.nullable) {
  zodSchema.expression = tsc.callExpression({
    functionName: tsc.propertyAccessExpression({
      expression: zodSchema.expression,
      name: identifiers.nullable,
    }),
  });
}

This would wrap nullable types appropriately, placing .nullable() inside .optional() to match the correct Zod v4 pattern.

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug πŸ”₯Something isn't workingjavascriptPull requests that update Javascript codeprioritized 🚚This issue has been prioritized and will be worked on soon

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions