-
Notifications
You must be signed in to change notification settings - Fork 143
Open
Labels
bugSomething isn't workingSomething isn't working
Description
Describe the bug and the expected behavior
When using coerceFormValue from @conform-to/zod/v4/future with a recursive Zod schema that uses the getter pattern recommended by Zod v4, the function enters an infinite recursion and throws a "Maximum call stack size exceeded" error.
The enableTypeCoercion function has logic to handle z.lazy() for recursive schemas by caching results to prevent infinite recursion. However, the getter-based approach recommended by Zod v4 bypasses this caching mechanism.
Conform version
v1.14.1
Steps to Reproduce the Bug or Issue
- Create a recursive schema using Zod v4's recommended getter pattern:
import { z } from 'zod'
import { coerceFormValue } from '@conform-to/zod/v4/future'
const ConditionNodeSchema = z.object({
type: z.literal('condition'),
value: z.string(),
})
const LogicalGroupNodeSchema = z.object({
type: z.literal('group'),
operator: z.enum(['AND', 'OR']),
// Getter pattern recommended by Zod v4: https://zod.dev/api#recursive-objects
get children(): z.ZodArray<z.ZodDiscriminatedUnion<[typeof LogicalGroupNodeSchema, typeof ConditionNodeSchema]>> {
return z.array(z.discriminatedUnion('type', [LogicalGroupNodeSchema, ConditionNodeSchema]))
},
})
// This throws: "Maximum call stack size exceeded"
const schema = coerceFormValue(
z.object({
filter: LogicalGroupNodeSchema,
})
)- Run the code - it will throw a stack overflow error.
What browsers are you seeing the problem on?
Chrome
Screenshots or Videos
No response
Additional context
My workaround is to wrap the recursive reference in z.lazy() inside the getter:
const LogicalGroupNodeSchema = z.object({
type: z.literal('group'),
operator: z.enum(['AND', 'OR']),
get children(): z.ZodArray<z.ZodLazy<z.ZodDiscriminatedUnion<[typeof LogicalGroupNodeSchema, typeof ConditionNodeSchema]>>> {
// Adding z.lazy() inside the getter allows coerceFormValue to work
return z.array(z.lazy(() => z.discriminatedUnion('type', [LogicalGroupNodeSchema, ConditionNodeSchema])))
},
})This workaround combines both patterns to satisfy TypeScript and enable proper caching in coerceFormValue.
Copilot
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working