Skip to content

Commit 6dadc14

Browse files
committed
Nested data traversal for correcting invalid types didn't stop at a valid value, replacing paths with default data further down the tree.
1 parent 3466d69 commit 6dadc14

File tree

4 files changed

+150
-5
lines changed

4 files changed

+150
-5
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ Headlines: Added, Changed, Deprecated, Removed, Fixed, Security
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Fixed
11+
12+
- Nested data traversal for correcting invalid types didn't stop at a valid value, replacing paths with default data further down the tree.
13+
814
## [2.27.1] - 2025-06-27
915

1016
### Fixed

src/lib/errors.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,10 +249,7 @@ export function replaceInvalidDefaults<T extends Record<string, unknown>>(
249249

250250
//#region Defaults
251251

252-
function Defaults_traverseAndReplace(
253-
defaultPath: Partial<PathData>,
254-
traversingErrors = false
255-
): void {
252+
function Defaults_traverseAndReplace(defaultPath: Partial<PathData>, traversingErrors = false) {
256253
const currentPath = defaultPath.path;
257254
if (!currentPath || !currentPath[0]) return;
258255
if (typeof currentPath[0] === 'string' && preprocessed?.includes(currentPath[0])) return;
@@ -292,7 +289,11 @@ export function replaceInvalidDefaults<T extends Record<string, unknown>>(
292289

293290
const fieldType = pathTypes.value ?? defaultType;
294291
if (fieldType) {
295-
Data_setValue(currentPath, Types_correctValue(dataValue, defValue, fieldType));
292+
const corrected = Types_correctValue(dataValue, defValue, fieldType);
293+
// If same value, skip the potential nested path as it's already correct.
294+
if (corrected === dataValue) return 'skip';
295+
296+
Data_setValue(currentPath, corrected);
296297
}
297298
}
298299
}

src/tests/zod4Union.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,88 @@ test('Default value with *matching* type in nested discriminated union with supe
259259
} satisfies FormSchema;
260260
await validate(data, FormSchema);
261261
});
262+
263+
test('Default value with *matching* type in nested discriminated union with superRefine', async () => {
264+
const ZodSchema2 = z
265+
.object({
266+
type: z.literal('additional'),
267+
additional: z
268+
.discriminatedUnion('type', [
269+
z.object({
270+
type: z.literal('same'),
271+
address: z.string().nullable()
272+
}),
273+
z.object({
274+
type: z.literal('different'),
275+
address: z.string()
276+
})
277+
])
278+
.default({
279+
type: 'same',
280+
address: null
281+
})
282+
})
283+
.superRefine((_data, ctx) => {
284+
ctx.addIssue({
285+
code: z.ZodIssueCode.custom,
286+
path: ['addresses', 'additional', 'name'],
287+
message: 'error'
288+
});
289+
});
290+
291+
const FormSchema = zod(ZodSchema2);
292+
type FormSchema = (typeof FormSchema)['defaults'];
293+
const data = {
294+
type: 'additional',
295+
additional: {
296+
type: 'different',
297+
address: '123 Main St'
298+
}
299+
} satisfies FormSchema;
300+
await validate(data, FormSchema);
301+
});
302+
303+
test('Keep valid value in nested discriminated union', async () => {
304+
const ZodSchema2 = z.object({
305+
type: z.literal('additional'),
306+
value: z.string(),
307+
extra: z.discriminatedUnion('type', [
308+
z.object({
309+
type: z.literal('num'),
310+
value: z.number().default(0)
311+
}),
312+
z.object({
313+
type: z.literal('complex'),
314+
value: z
315+
.discriminatedUnion('type', [
316+
z.object({
317+
type: z.literal('string'),
318+
value: z.string()
319+
}),
320+
z.object({
321+
type: z.literal('number'),
322+
value: z.number()
323+
})
324+
])
325+
.default({
326+
type: 'string',
327+
value: ''
328+
})
329+
})
330+
])
331+
});
332+
333+
const FormSchema = zod(ZodSchema2);
334+
type FormSchema = (typeof FormSchema)['defaults'];
335+
const data = {
336+
type: 'additional',
337+
value: 'test',
338+
extra: {
339+
type: 'num',
340+
value: 42
341+
}
342+
} satisfies FormSchema;
343+
const result = await validate(data, FormSchema);
344+
345+
expect(result.data.extra.value).toBe(42);
346+
});

src/tests/zodUnion.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,56 @@ test('Default value with *matching* type in nested discriminated union with supe
160160
} satisfies FormSchema;
161161
await validate(data, FormSchema);
162162
});
163+
164+
test('Keep valid value in nested discriminated union with superRefine', async () => {
165+
const ZodSchema2 = z
166+
.object({
167+
type: z.literal('additional'),
168+
value: z.string(),
169+
extra: z.discriminatedUnion('type', [
170+
z.object({
171+
type: z.literal('num'),
172+
value: z.number().default(0)
173+
}),
174+
z.object({
175+
type: z.literal('complex'),
176+
value: z
177+
.discriminatedUnion('type', [
178+
z.object({
179+
type: z.literal('string'),
180+
value: z.string()
181+
}),
182+
z.object({
183+
type: z.literal('number'),
184+
value: z.number()
185+
})
186+
])
187+
.default({
188+
type: 'string',
189+
value: ''
190+
})
191+
})
192+
])
193+
})
194+
.superRefine((_data, ctx) => {
195+
ctx.addIssue({
196+
code: z.ZodIssueCode.custom,
197+
path: ['value'],
198+
message: 'error'
199+
});
200+
});
201+
202+
const FormSchema = zod(ZodSchema2);
203+
type FormSchema = (typeof FormSchema)['defaults'];
204+
const data = {
205+
type: 'additional',
206+
value: 'test',
207+
extra: {
208+
type: 'num',
209+
value: 42
210+
}
211+
} satisfies FormSchema;
212+
const result = await validate(data, FormSchema);
213+
214+
expect(result.data.extra.value).toBe(42);
215+
});

0 commit comments

Comments
 (0)