Skip to content

zod v4: .default(...) on enum fields emits a bare string literal that is not assignable to the referenced TS enum (regression in 0.97.2) #3937

@einarwar

Description

@einarwar

Summary

After upgrading from 0.97.1 → 0.97.2, the generated Zod file no longer typechecks for any object property that combines:

  1. an $ref to a string enum schema, and
  2. a default value.

This appears to be an unintended side-effect of #3884 ("use enums from TypeScript if available"). The Zod plugin now references the TypeScript-emitted enum symbol (great!), but the .default(...) emission was not updated to match — it still emits a bare string literal, which is not assignable to the TS enum type.

Versions

  • @hey-api/openapi-ts: 0.97.2 (also reproduces on 0.97.3)
  • zod: v4
  • typescript: 5.x
  • @hey-api/typescript plugin enabled (so TS enums are emitted)

What gets generated

Given an OpenAPI string enum referenced by an object property that has a default, e.g. (illustrative):

components:
  schemas:
    Color:
      type: string
      enum: [RED, GREEN, BLUE]
    Paint:
      type: object
      properties:
        primary:
          allOf: [{ $ref: '#/components/schemas/Color' }]
          default: RED

types.gen.ts emits a native TS enum:

export enum Color {
  RED = 'RED',
  GREEN = 'GREEN',
  BLUE = 'BLUE',
}

zod.gen.ts (0.97.2+) emits:

import { Color } from './types.gen';

export const zColor = z.enum(Color);

export const zPaint = z.object({
  primary: zColor.optional().default('RED'), // ← TS error
});

Observed error

error TS2769: No overload matches this call.
  Overload 1 of 2, '(def: Color): ZodDefault<ZodOptional<ZodEnum<typeof Color>>>', gave the following error.
    Argument of type '"RED"' is not assignable to parameter of type 'Color'.
  Overload 2 of 2, '(def: () => Color): ZodDefault<ZodOptional<ZodEnum<typeof Color>>>', gave the following error.
    Argument of type 'string' is not assignable to parameter of type '() => Color'.

Why it fails

In Zod v4, z.enum(NativeEnum) produces ZodEnum<typeof NativeEnum> whose inferred output type is the TS enum type (Color), not the string-literal union. TS string enums are nominal, so the bare literal 'RED' is not assignable to Color even though they are equal at runtime.

In 0.97.1 the generator emitted z.enum(['RED', 'GREEN', 'BLUE']), whose output is the string-literal union 'RED' | 'GREEN' | 'BLUE' and accepts .default('RED') fine. The switch to z.enum(TsEnum) (intended) was not paired with switching the default emission (unintended).

Reproduction

Any spec containing:

  • a top-level string enum schema (so a TS enum is emitted by @hey-api/typescript), and
  • another schema with a property that $refs that enum and specifies a default.

Configure both @hey-api/typescript and zod plugins, run codegen, then tsc --noEmit. The error appears on every such default. Same pattern also breaks for the kind discriminator defaults emitted by the SDK validator path.

Suggested fix

Emit defaults as enum-member references rather than string literals when the schema is rendered as z.enum(TsEnum) (and import the enum as a value, not just a type):

import { Color } from './types.gen';

primary: zColor.optional().default(Color.RED),

Alternatively, fall back to inline literal arrays (z.enum(['RED', ...])) when a default is present on a referencing property — but the member-reference fix is cleaner and preserves the new symbol-reuse behavior from #3884.

The same issue applies to the valibot plugin path that was changed in the same PR; haven't verified there but the code path looks symmetric.

Workaround

Pin to @hey-api/openapi-ts@0.97.1 until a fix is released.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug 🔥Broken or incorrect behavior.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions