Skip to content

zod/v4: Non JSONSchema representable types used with default() will always initialize to empty object {} #617

@kaechele

Description

@kaechele

Description

Take for example z.set(z.number()).default(new Set()).
This pre-populates form data with an empty object ({}) instead of a Set (Set(0) {}) as its default value.
Likewise, z.set(z.number()).default(new Set([1, 2, 3])) also generates {} instead of the expected Set(3) { 1, 2, 3 }.

If relying on the form data having a proper Set, possibly with pre-filled values, in its initial state (i.e. using default values from zod), this will break things that access any Set specific members, such as Set.has().

This is a regression from using superforms with zod v3. It works fine there.

Root cause is the new toJSONSchema function used in the zod4 adapter. Due to the override function in superforms using the unrepresentable: 'any' option any unrepresentable object will become an empty object ({}).

Comparison of v3 and v4 generated JSON Schema

schema.ts:

const myDefaultSet = z.object({
  set: z.set(z.number()).default(new Set([1, 2, 3])),
});

v3

{
  type: 'object',
  properties: {
    set: {
      type: 'array',
      uniqueItems: true,
      items: { type: 'number' },
      default: Set(3) { 1, 2, 3 }
    }
  },
  additionalProperties: false,
  '$schema': 'http://json-schema.org/draft-07/schema#'
}

v4 with superforms' default override options

{
  '$schema': 'https://json-schema.org/draft/2020-12/schema',
  type: 'object',
  properties: { set: { default: {} } },
  required: [ 'set' ],
  additionalProperties: false
}

Thoughts

I looked into providing a fix myself by modifying the override function. However, after studying zod's v4 to-json-schema.ts I concluded this will get messy quick due to the needed state tracking and recursion.
The reason is that z.default() in schema generation is parent to z.set(z.number), and the z.set(z.number) def does not seem to have any indication of that.
This means, that while processing the z.default, which has the desired defaultValue (in def.defaultValue) that we need to put into the custom JSON Schema, we can see that there is a def.innerType of z.set(z.number()) but we cannot alter that node (as it is actually processed first and already in the schema at that point). That default value is unfortunately also not found in the child z.set(z.number) def.

In summary, this issue will affect all non-JSONSchema-representable types that are used with default() as well, not just Set().

Metadata

Metadata

Assignees

No one assigned

    Labels

    adapterRelated to validation adaptersbugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions