Skip to content

Commit 5d2fb0f

Browse files
committed
Fixed Default Date values in nested objects
1 parent 5e3ec3c commit 5d2fb0f

File tree

3 files changed

+153
-4
lines changed

3 files changed

+153
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Zod 4 adapter: Allow top-level `.transform()` and `.refine()` in schemas. [#646](https://github.com/ciscoheat/sveltekit-superforms/issues/646).
1313
- Zod 4 adapter: Now respects global `customError` configuration when no explicit error map is provided. The adapter prioritizes `customError` over `localeError`. [#618](https://github.com/ciscoheat/sveltekit-superforms/issues/618).
14+
- Zod 4 adapter: Fixed Default Date values in nested objects. [#650](https://github.com/ciscoheat/sveltekit-superforms/issues/650).
1415

1516
## [2.28.0] - 2025-10-19
1617

src/lib/jsonSchema/schemaDefaults.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,47 @@ function _defaultValues(schema: JSONSchema, isOptional: boolean, path: string[])
127127
for (const [key, objSchema] of Object.entries(info.properties)) {
128128
assertSchema(objSchema, [...path, key]);
129129

130-
const def =
131-
objectDefaults && objectDefaults[key] !== undefined
132-
? objectDefaults[key]
133-
: _defaultValues(objSchema, !info.required?.includes(key), [...path, key]);
130+
// Determine the default value for this property. If an object-level default
131+
// was provided, attempt to convert it to the correct typed form using the
132+
// property's schema information (e.g. convert ISO strings back to Date).
133+
let def;
134+
if (objectDefaults && objectDefaults[key] !== undefined) {
135+
try {
136+
const propInfo = schemaInfo(objSchema, !info.required?.includes(key), [...path, key]);
137+
if (propInfo) {
138+
// Use the first resolved type to format simple values (dates, sets, maps, bigint, symbol)
139+
const propType = propInfo.types[0];
140+
// If property is an object and the provided default is an object,
141+
// we need to recursively process it to convert nested values (e.g. Date strings to Dates)
142+
if (
143+
propType === 'object' &&
144+
typeof objectDefaults[key] === 'object' &&
145+
objectDefaults[key] !== null &&
146+
!Array.isArray(objectDefaults[key])
147+
) {
148+
// Create a temporary schema with the parent's default to recursively process it
149+
const schemaWithDefault: JSONSchema = {
150+
...objSchema,
151+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
152+
default: objectDefaults[key] as any
153+
};
154+
def = _defaultValues(schemaWithDefault, !info.required?.includes(key), [
155+
...path,
156+
key
157+
]);
158+
} else {
159+
def = formatDefaultValue(propType, objectDefaults[key]);
160+
}
161+
} else {
162+
def = objectDefaults[key];
163+
}
164+
} catch {
165+
// If anything goes wrong determining/formatting, fall back to provided value
166+
def = objectDefaults[key];
167+
}
168+
} else {
169+
def = _defaultValues(objSchema, !info.required?.includes(key), [...path, key]);
170+
}
134171

135172
//if (def !== undefined) output[key] = def;
136173
if (output === undefined) output = {};

src/tests/issue-650.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { superValidate } from '$lib/superValidate.js';
3+
import { zod } from '$lib/adapters/zod4.js';
4+
import { z } from 'zod/v4';
5+
6+
describe('Issue 650 - Zod 4 Default Date values in nested objects', () => {
7+
it('should handle default dates in nested objects correctly', async () => {
8+
const schema = z.object({
9+
name: z.string().min(2),
10+
email: z.string().email(),
11+
dateRange: z
12+
.object({
13+
start: z.date(),
14+
end: z.date()
15+
})
16+
.default({
17+
start: new Date('2025-10-29T10:47:03.068Z'),
18+
end: new Date('2025-10-29T10:47:03.068Z')
19+
}),
20+
dateRangeIndividual: z.object({
21+
start: z.date().default(new Date('2025-10-29T10:52:25.537Z')),
22+
end: z.date().default(new Date('2025-10-29T10:52:25.537Z'))
23+
}),
24+
date: z.date().default(new Date('2025-10-29T10:47:03.068Z'))
25+
});
26+
27+
const form = await superValidate(zod(schema));
28+
const data = form.data as any; // eslint-disable-line @typescript-eslint/no-explicit-any
29+
30+
// All dates should be Date objects, not strings
31+
expect(data.dateRange.start).toBeInstanceOf(Date);
32+
expect(data.dateRange.end).toBeInstanceOf(Date);
33+
expect(data.dateRangeIndividual.start).toBeInstanceOf(Date);
34+
expect(data.dateRangeIndividual.end).toBeInstanceOf(Date);
35+
expect(data.date).toBeInstanceOf(Date);
36+
37+
// Verify the actual date values match what was set
38+
expect(data.dateRange.start.toISOString()).toBe('2025-10-29T10:47:03.068Z');
39+
expect(data.dateRange.end.toISOString()).toBe('2025-10-29T10:47:03.068Z');
40+
expect(data.dateRangeIndividual.start.toISOString()).toBe('2025-10-29T10:52:25.537Z');
41+
expect(data.dateRangeIndividual.end.toISOString()).toBe('2025-10-29T10:52:25.537Z');
42+
expect(data.date.toISOString()).toBe('2025-10-29T10:47:03.068Z');
43+
});
44+
45+
it('should handle nested objects with default dates at multiple levels', async () => {
46+
const schema = z.object({
47+
level1: z
48+
.object({
49+
level2: z
50+
.object({
51+
date: z.date()
52+
})
53+
.default({
54+
date: new Date('2025-01-01T00:00:00.000Z')
55+
})
56+
})
57+
.default({
58+
level2: {
59+
date: new Date('2025-01-01T00:00:00.000Z')
60+
}
61+
})
62+
});
63+
64+
const form = await superValidate(zod(schema));
65+
const data = form.data as any; // eslint-disable-line @typescript-eslint/no-explicit-any
66+
67+
expect(data.level1.level2.date).toBeInstanceOf(Date);
68+
expect(data.level1.level2.date.toISOString()).toBe('2025-01-01T00:00:00.000Z');
69+
});
70+
71+
it('should handle mixed default values in nested objects', async () => {
72+
const schema = z.object({
73+
settings: z
74+
.object({
75+
theme: z.string(),
76+
createdAt: z.date(),
77+
count: z.number()
78+
})
79+
.default({
80+
theme: 'dark',
81+
createdAt: new Date('2025-10-29T00:00:00.000Z'),
82+
count: 42
83+
})
84+
});
85+
86+
const form = await superValidate(zod(schema));
87+
const data = form.data as any; // eslint-disable-line @typescript-eslint/no-explicit-any
88+
89+
expect(data.settings.theme).toBe('dark');
90+
expect(data.settings.createdAt).toBeInstanceOf(Date);
91+
expect(data.settings.createdAt.toISOString()).toBe('2025-10-29T00:00:00.000Z');
92+
expect(data.settings.count).toBe(42);
93+
});
94+
95+
it('should preserve individual field defaults when object has no default', async () => {
96+
const schema = z.object({
97+
timestamps: z.object({
98+
created: z.date().default(new Date('2025-01-01T00:00:00.000Z')),
99+
updated: z.date().default(new Date('2025-01-02T00:00:00.000Z'))
100+
})
101+
});
102+
103+
const form = await superValidate(zod(schema));
104+
const data = form.data as any; // eslint-disable-line @typescript-eslint/no-explicit-any
105+
106+
expect(data.timestamps.created).toBeInstanceOf(Date);
107+
expect(data.timestamps.updated).toBeInstanceOf(Date);
108+
expect(data.timestamps.created.toISOString()).toBe('2025-01-01T00:00:00.000Z');
109+
expect(data.timestamps.updated.toISOString()).toBe('2025-01-02T00:00:00.000Z');
110+
});
111+
});

0 commit comments

Comments
 (0)