Skip to content

Commit b02f5d0

Browse files
committed
Zod 4 fixes
1 parent 680c203 commit b02f5d0

File tree

10 files changed

+175
-10
lines changed

10 files changed

+175
-10
lines changed

CHANGELOG.md

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

1010
### Added
1111

12-
- [Zod 4](https://zod.dev/) adapter added! Adapter name is `zod4`. As Zod is now using its own JSON Schema representation, and there are some breaking changes like how enums are handled, check migrations carefully and please report any bugs [on github](https://github.com/ciscoheat/sveltekit-superforms/issues).
12+
- [Zod 4](https://zod.dev/) adapter added! Adapter names are `zod4` and `zod4Client`, works with both the full version and Zod Mini. As Zod is now using its own JSON Schema representation, and there are some breaking changes like how enums are handled, check migrations carefully and please [report any bugs](https://github.com/ciscoheat/sveltekit-superforms/issues).
1313
- [taintedMessage](https://superforms.rocks/concepts/tainted) now passes `BeforeNavigate` in its function signature.
1414

1515
### Changed

src/lib/adapters/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export {
1515
type ZodObjectTypes,
1616
type ZodObjectType
1717
} from './zod.js';
18+
export { zod as zod4, zodClient as zod4Client } from './zod4.js';
1819
export { vine, vineClient } from './vine.js';
1920
export { schemasafe, schemasafeClient } from './schemasafe.js';
2021
export { superstruct, superstructClient } from './superstruct.js';

src/lib/adapters/zod4.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import { memoize } from '$lib/memoize.js';
1313

1414
type Options = NonNullable<Parameters<typeof toJSONSchema>[1]>;
1515

16-
type ZodObject = $ZodObject;
17-
1816
const defaultJSONSchemaOptions = {
1917
unrepresentable: 'any',
2018
override: (ctx) => {
@@ -27,11 +25,11 @@ const defaultJSONSchemaOptions = {
2725
} satisfies Options;
2826

2927
/* @__NO_SIDE_EFFECTS__ */
30-
export const zodToJSONSchema = <S extends ZodObject>(schema: S, options?: Options) => {
28+
export const zodToJSONSchema = <S extends $ZodObject>(schema: S, options?: Options) => {
3129
return toJSONSchema(schema, { ...defaultJSONSchemaOptions, ...options }) as JSONSchema7;
3230
};
3331

34-
async function validate<T extends ZodObject>(
32+
async function validate<T extends $ZodObject>(
3533
schema: T,
3634
data: unknown,
3735
error: $ZodErrorMap | undefined
@@ -50,7 +48,7 @@ async function validate<T extends ZodObject>(
5048
};
5149
}
5250

53-
function _zod4<T extends ZodObject>(
51+
function _zod4<T extends $ZodObject>(
5452
schema: T,
5553
options?: AdapterOptions<Infer<T, 'zod4'>> & { error?: $ZodErrorMap; config?: Options }
5654
): ValidationAdapter<Infer<T, 'zod4'>, InferIn<T, 'zod4'>> {
@@ -64,7 +62,7 @@ function _zod4<T extends ZodObject>(
6462
});
6563
}
6664

67-
function _zod4Client<T extends ZodObject>(
65+
function _zod4Client<T extends $ZodObject>(
6866
schema: T,
6967
options?: { error?: $ZodErrorMap }
7068
): ClientValidationAdapter<Infer<T, 'zod4'>, InferIn<T, 'zod4'>> {
@@ -74,5 +72,5 @@ function _zod4Client<T extends ZodObject>(
7472
};
7573
}
7674

77-
export const zod4 = /* @__PURE__ */ memoize(_zod4);
78-
export const zod4Client = /* @__PURE__ */ memoize(_zod4Client);
75+
export const zod = /* @__PURE__ */ memoize(_zod4);
76+
export const zodClient = /* @__PURE__ */ memoize(_zod4Client);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { zod } from '$lib/adapters/zod4.js';
2+
import { message, superValidate } from '$lib/server/index.js';
3+
import { schema } from './schema.js';
4+
import { fail } from '@sveltejs/kit';
5+
6+
export const load = async () => {
7+
const form = await superValidate(zod(schema));
8+
return { form };
9+
};
10+
11+
export const actions = {
12+
default: async ({ request }) => {
13+
const formData = await request.formData();
14+
console.log(formData);
15+
16+
const form = await superValidate(formData, zod(schema));
17+
console.log(form);
18+
19+
if (!form.valid) return fail(400, { form });
20+
21+
return message(form, 'Posted OK!');
22+
}
23+
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<script lang="ts">
2+
import { superForm } from '$lib/client/index.js';
3+
import SuperDebug from '$lib/client/SuperDebug.svelte';
4+
import { zod } from '$lib/adapters/zod4.js';
5+
import { schema } from './schema.js';
6+
7+
export let data;
8+
9+
const { form, errors, tainted, message, enhance } = superForm(data.form, {
10+
taintedMessage: false,
11+
validators: zod(schema)
12+
//dataType: 'json',
13+
});
14+
</script>
15+
16+
<SuperDebug data={{ $form, $errors, $tainted }} />
17+
18+
{#if $message}<h4>{$message}</h4>{/if}
19+
20+
<form method="POST" use:enhance>
21+
<label>
22+
Name: <input
23+
name="name"
24+
bind:value={$form.name}
25+
aria-invalid={$errors.name ? 'true' : undefined}
26+
/>
27+
{#if $errors.name}<span class="invalid">{$errors.name}</span>{/if}
28+
</label>
29+
<label>
30+
Email: <input
31+
name="email"
32+
bind:value={$form.email}
33+
aria-invalid={$errors.email ? 'true' : undefined}
34+
/>
35+
{#if $errors.email}<span class="invalid">{$errors.email}</span>{/if}
36+
</label>
37+
<div>
38+
<button>Submit</button>
39+
</div>
40+
</form>
41+
42+
<style lang="scss">
43+
form {
44+
margin: 2rem 0;
45+
46+
input {
47+
background-color: #dedede;
48+
}
49+
50+
.invalid {
51+
color: crimson;
52+
}
53+
}
54+
</style>

src/routes/(v2)/v2/zod4/schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { z } from 'zod/v4';
2+
3+
export const schema = z.object({
4+
name: z.string().min(2),
5+
email: z.string().email()
6+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { zod } from '$lib/adapters/zod4.js';
2+
import { message, superValidate } from '$lib/server/index.js';
3+
import { schema } from './schema.js';
4+
import { fail } from '@sveltejs/kit';
5+
6+
export const load = async () => {
7+
const form = await superValidate(zod(schema));
8+
return { form };
9+
};
10+
11+
export const actions = {
12+
default: async ({ request }) => {
13+
const formData = await request.formData();
14+
console.log(formData);
15+
16+
const form = await superValidate(formData, zod(schema));
17+
console.log(form);
18+
19+
if (!form.valid) return fail(400, { form });
20+
21+
return message(form, 'Posted OK!');
22+
}
23+
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<script lang="ts">
2+
import { superForm } from '$lib/client/index.js';
3+
import SuperDebug from '$lib/client/SuperDebug.svelte';
4+
import { zod } from '$lib/adapters/zod4.js';
5+
import { schema } from './schema.js';
6+
7+
export let data;
8+
9+
const { form, errors, tainted, message, enhance } = superForm(data.form, {
10+
taintedMessage: false,
11+
//dataType: 'json',
12+
validators: zod(schema)
13+
});
14+
</script>
15+
16+
<SuperDebug data={{ $form, $errors, $tainted }} />
17+
18+
{#if $message}<h4>{$message}</h4>{/if}
19+
20+
<form method="POST" use:enhance>
21+
<label>
22+
Name: <input
23+
name="name"
24+
bind:value={$form.name}
25+
aria-invalid={$errors.name ? 'true' : undefined}
26+
/>
27+
{#if $errors.name}<span class="invalid">{$errors.name}</span>{/if}
28+
</label>
29+
<label>
30+
Email: <input
31+
name="email"
32+
bind:value={$form.email}
33+
aria-invalid={$errors.email ? 'true' : undefined}
34+
/>
35+
{#if $errors.email}<span class="invalid">{$errors.email}</span>{/if}
36+
</label>
37+
<div>
38+
<button>Submit</button>
39+
</div>
40+
</form>
41+
42+
<style lang="scss">
43+
form {
44+
margin: 2rem 0;
45+
46+
input {
47+
background-color: #dedede;
48+
}
49+
50+
.invalid {
51+
color: crimson;
52+
}
53+
}
54+
</style>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { z } from 'zod/v4-mini';
2+
3+
export const schema = z.object({
4+
name: z.string().check(z.minLength(2, 'Name is too short')),
5+
email: z.email('Invalid email')
6+
});

src/tests/superValidate.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { defaults as schemaDefaults } from '$lib/defaults.js';
2020
import { zod, zodToJSONSchema } from '$lib/adapters/zod.js';
2121
import { z, type ZodErrorMap } from 'zod';
2222

23-
import { zod4, zodToJSONSchema as zod4ToJSONSchema } from '$lib/adapters/zod4.js';
23+
import { zod as zod4, zodToJSONSchema as zod4ToJSONSchema } from '$lib/adapters/zod4.js';
2424
import { z as z4 } from 'zod/v4';
2525

2626
import { valibot } from '$lib/adapters/valibot.js';

0 commit comments

Comments
 (0)