Skip to content

fix: cascade inner field defaults through .default({}) on objects#5768

Open
mahmoodhamdi wants to merge 1 commit intocolinhacks:mainfrom
mahmoodhamdi:fix/default-cascade-inner-defaults
Open

fix: cascade inner field defaults through .default({}) on objects#5768
mahmoodhamdi wants to merge 1 commit intocolinhacks:mainfrom
mahmoodhamdi:fix/default-cascade-inner-defaults

Conversation

@mahmoodhamdi
Copy link

Summary

Fixes #5764

When .default({}) is called on an object schema, the default value is now parsed through the inner schema at definition time so that nested field-level defaults are applied. This ensures the invariant:

schema.default(d).parse(undefined) === schema.parse(d)

holds for object schemas, matching user expectations and Zod v4's own direction on defaults.

Before

const schema = z.object({
  name: z.string().default("untitled"),
  count: z.number().default(0),
}).default({});

schema.parse(undefined); // => {} ❌

After

schema.parse(undefined); // => { name: "untitled", count: 0 } ✅

Approach

In the _default() factory (classic, mini, and core), when the inner type is an object schema and the default is a static plain object (not a function), run it through the inner schema's parse at definition time. This:

  • Has zero runtime overhead — the short-circuit optimization in $ZodDefault.parse is preserved
  • Handles deeply nested objects recursively (inner defaults are already resolved when outer .default({}) is called)
  • Silently falls back to the original default if parsing fails (e.g., invalid default)

Files changed

  • packages/zod/src/v4/classic/schemas.ts — classic _default() factory
  • packages/zod/src/v4/mini/schemas.ts — mini _default() factory
  • packages/zod/src/v4/core/api.ts — core _default() factory
  • packages/zod/src/v4/classic/tests/default.test.ts — 4 new test cases

Test plan

  • Existing default.test.ts tests still pass (20/20)
  • New tests cover: basic cascading, deeply nested objects, partial overrides, shallow clone identity
  • Full test suite passes (3583 tests, 0 type errors)
  • Format and lint checks pass

When .default({}) is called on an object schema, the default value is
now parsed through the inner schema at definition time so that nested
field-level defaults are applied. This ensures the invariant that
schema.default(d).parse(undefined) === schema.parse(d) holds for
object schemas.

Fixes colinhacks#5764
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.

.default({}) on objects should cascade inner field defaults

1 participant