Skip to content

Commit 18b0dfc

Browse files
committed
fixup
1 parent 980a098 commit 18b0dfc

File tree

2 files changed

+19
-26
lines changed

2 files changed

+19
-26
lines changed

packages/conform-zod/v4/coercion.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -177,16 +177,13 @@ export function enableTypeCoercion<Schema extends $ZodType>(
177177
): $ZodType {
178178
const result = options.cache.get(type);
179179

180-
// Return the cached schema if it's already processed
181-
// This is to prevent infinite recursion caused by z.lazy() or getter-based recursive schemas
182180
if (result) {
183181
return result;
184182
}
185183

186-
// Pre-cache the schema to handle recursive references
187-
// This prevents infinite recursion when processing getter-based recursive schemas
188-
// The cache will be updated with the final coerced schema after processing
189-
options.cache.set(type, type);
184+
// Pre-cache a lazy wrapper so recursive references resolve to the coerced schema
185+
const lazyCoerced = lazy(() => options.cache.get(type) ?? type);
186+
options.cache.set(type, lazyCoerced);
190187

191188
let schema: $ZodType = type;
192189
const zod = (type as unknown as $ZodTypes)._zod;
@@ -303,16 +300,15 @@ export function enableTypeCoercion<Schema extends $ZodType>(
303300
return object;
304301
};
305302

306-
// Check if item is already in cache to prevent infinite recursion
307-
// This handles recursive discriminated unions with getter-based schemas
303+
// Handle recursive references by wrapping the cached lazy to preserve propValues
308304
const cachedItem = options.cache.get(item);
309-
if (cachedItem) {
310-
// If the cached item is the same as the original, it already has the correct propValues
311-
// Don't try to modify read-only properties
312-
if (cachedItem === item) {
313-
return item;
314-
}
315-
return setOriginalPropValues(cachedItem);
305+
if (cachedItem && cachedItem !== item) {
306+
return setOriginalPropValues(
307+
pipe(
308+
transform((value) => value),
309+
cachedItem,
310+
),
311+
);
316312
}
317313

318314
if (objectDef.type !== 'object') {
@@ -361,9 +357,7 @@ export function enableTypeCoercion<Schema extends $ZodType>(
361357
schema = lazy(() => enableTypeCoercion(inner, options));
362358
}
363359

364-
if (type !== schema) {
365-
options.cache.set(type, schema);
366-
}
360+
options.cache.set(type, schema);
367361

368362
return schema;
369363
}

packages/conform-zod/v4/tests/coercion/schema/lazy.test.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,16 @@ describe('coercion', () => {
9999
});
100100

101101
test('should handle getter-based recursive schemas', () => {
102-
// Test case from issue: https://github.com/edmundhung/conform/issues/1127
103-
// Zod v4 recommended getter pattern: https://zod.dev/api#recursive-objects
102+
// https://github.com/edmundhung/conform/issues/1127
104103
const ConditionNodeSchema = z.object({
105104
type: z.literal('condition'),
106105
value: z.string(),
106+
priority: z.number(),
107107
});
108108

109109
const LogicalGroupNodeSchema = z.object({
110110
type: z.literal('group'),
111111
operator: z.enum(['AND', 'OR']),
112-
// Getter pattern recommended by Zod v4
113112
get children(): z.ZodArray<
114113
z.ZodDiscriminatedUnion<
115114
[typeof LogicalGroupNodeSchema, typeof ConditionNodeSchema]
@@ -124,14 +123,12 @@ describe('coercion', () => {
124123
},
125124
});
126125

127-
// This should not throw "Maximum call stack size exceeded"
128126
const schema = coerceFormValue(
129127
z.object({
130128
filter: LogicalGroupNodeSchema,
131129
}),
132130
);
133131

134-
// Test parsing with valid data
135132
expect(
136133
getResult(
137134
schema.safeParse({
@@ -142,6 +139,7 @@ describe('coercion', () => {
142139
{
143140
type: 'condition',
144141
value: 'test',
142+
priority: '1',
145143
},
146144
],
147145
},
@@ -157,13 +155,13 @@ describe('coercion', () => {
157155
{
158156
type: 'condition',
159157
value: 'test',
158+
priority: 1,
160159
},
161160
],
162161
},
163162
},
164163
});
165164

166-
// Test parsing with nested recursive structure
167165
expect(
168166
getResult(
169167
schema.safeParse({
@@ -178,6 +176,7 @@ describe('coercion', () => {
178176
{
179177
type: 'condition',
180178
value: 'nested',
179+
priority: '99',
181180
},
182181
],
183182
},
@@ -199,6 +198,7 @@ describe('coercion', () => {
199198
{
200199
type: 'condition',
201200
value: 'nested',
201+
priority: 99,
202202
},
203203
],
204204
},
@@ -207,7 +207,6 @@ describe('coercion', () => {
207207
},
208208
});
209209

210-
// Test validation errors
211210
const errorResult = getResult(
212211
schema.safeParse({
213212
filter: {
@@ -216,7 +215,6 @@ describe('coercion', () => {
216215
children: [
217216
{
218217
type: 'condition',
219-
// missing 'value'
220218
},
221219
],
222220
},
@@ -225,6 +223,7 @@ describe('coercion', () => {
225223
expect(errorResult.success).toEqual(false);
226224
if (!errorResult.success) {
227225
expect(errorResult.error['filter.children[0].value']).toBeDefined();
226+
expect(errorResult.error['filter.children[0].priority']).toBeDefined();
228227
}
229228
});
230229
});

0 commit comments

Comments
 (0)