Skip to content

Commit 8ed39b7

Browse files
committed
Zod 4 adapter now handles top-level discriminated unions.
1 parent 2943030 commit 8ed39b7

File tree

4 files changed

+97
-6
lines changed

4 files changed

+97
-6
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+
- Zod 4 adapter now handles top-level discriminated unions.
13+
814
## [2.26.0] - 2025-06-03
915

1016
### Added

src/lib/adapters/index.ts

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

src/lib/adapters/zod4.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { type $ZodObject, type $ZodErrorMap, safeParseAsync, toJSONSchema } from 'zod/v4/core';
1+
import {
2+
type $ZodObject,
3+
type $ZodErrorMap,
4+
safeParseAsync,
5+
toJSONSchema,
6+
$ZodDiscriminatedUnion
7+
} from 'zod/v4/core';
28
import type { JSONSchema7 } from 'json-schema';
39
import {
410
type AdapterOptions,
@@ -13,6 +19,8 @@ import { memoize } from '$lib/memoize.js';
1319

1420
type Options = NonNullable<Parameters<typeof toJSONSchema>[1]>;
1521

22+
export type ZodValidationSchema = $ZodObject | $ZodDiscriminatedUnion<$ZodObject[]>;
23+
1624
const defaultJSONSchemaOptions = {
1725
unrepresentable: 'any',
1826
override: (ctx) => {
@@ -25,11 +33,11 @@ const defaultJSONSchemaOptions = {
2533
} satisfies Options;
2634

2735
/* @__NO_SIDE_EFFECTS__ */
28-
export const zodToJSONSchema = <S extends $ZodObject>(schema: S, options?: Options) => {
36+
export const zodToJSONSchema = <S extends ZodValidationSchema>(schema: S, options?: Options) => {
2937
return toJSONSchema(schema, { ...defaultJSONSchemaOptions, ...options }) as JSONSchema7;
3038
};
3139

32-
async function validate<T extends $ZodObject>(
40+
async function validate<T extends ZodValidationSchema>(
3341
schema: T,
3442
data: unknown,
3543
error: $ZodErrorMap | undefined
@@ -48,7 +56,7 @@ async function validate<T extends $ZodObject>(
4856
};
4957
}
5058

51-
function _zod4<T extends $ZodObject>(
59+
function _zod4<T extends ZodValidationSchema>(
5260
schema: T,
5361
options?: AdapterOptions<Infer<T, 'zod4'>> & { error?: $ZodErrorMap; config?: Options }
5462
): ValidationAdapter<Infer<T, 'zod4'>, InferIn<T, 'zod4'>> {
@@ -62,7 +70,7 @@ function _zod4<T extends $ZodObject>(
6270
});
6371
}
6472

65-
function _zod4Client<T extends $ZodObject>(
73+
function _zod4Client<T extends ZodValidationSchema>(
6674
schema: T,
6775
options?: { error?: $ZodErrorMap }
6876
): ClientValidationAdapter<Infer<T, 'zod4'>, InferIn<T, 'zod4'>> {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<script lang="ts">
2+
import { z } from 'zod/v4';
3+
import { superForm, defaults } from '$lib/index.js';
4+
import { zod as zod4 } from '$lib/adapters/zod4.js';
5+
6+
// ==================== VALIDATORS ====================
7+
8+
/**
9+
* Base validator for common UI schema properties.
10+
*/
11+
const BaseUiSchemaItemValidator = z.object({
12+
defaultValueCompute: z.string().nullable(),
13+
dynamicValueCompute: z
14+
.object({
15+
dependencies: z.array(z.string()),
16+
fn: z.string()
17+
})
18+
.nullable()
19+
});
20+
21+
/**
22+
* Text UI Schema variant
23+
*/
24+
const TextUiSchemaValidator = BaseUiSchemaItemValidator.extend({
25+
dataType: z.literal('text'),
26+
selectedWidget: z.enum(['text_input', 'textarea']).default('text_input')
27+
});
28+
29+
/**
30+
* Boolean UI Schema variant
31+
*/
32+
const BoolUiSchemaValidator = BaseUiSchemaItemValidator.extend({
33+
dataType: z.literal('bool'),
34+
selectedWidget: z.enum(['checkbox', 'toggle']).default('checkbox')
35+
});
36+
37+
/**
38+
* DISCRIMINATED UNION - This is what causes the error
39+
* TypeError: Cannot read properties of undefined (reading '_zod')
40+
*/
41+
const UiSchemaItemValidator = z.discriminatedUnion('dataType', [
42+
TextUiSchemaValidator,
43+
BoolUiSchemaValidator
44+
]);
45+
46+
// ==================== SUPERFORM SETUP ====================
47+
48+
// THIS IS WHERE THE ERROR OCCURS
49+
const data = defaults(zod4(UiSchemaItemValidator));
50+
const { form, enhance } = superForm(data, {
51+
SPA: true,
52+
resetForm: false,
53+
dataType: 'json',
54+
validators: zod4(UiSchemaItemValidator),
55+
id: crypto.randomUUID(),
56+
validationMethod: 'submit-only',
57+
onUpdate: async (e) => {
58+
console.log(e.form.data);
59+
}
60+
});
61+
</script>
62+
63+
<!-- ==================== TEMPLATE ==================== -->
64+
65+
<form method="POST" use:enhance>
66+
{#if $form.dataType === 'text'}
67+
<input type="text" bind:value={$form.selectedWidget} name="selectedWidget" />
68+
{:else if $form.dataType === 'bool'}
69+
<select bind:value={$form.selectedWidget} name="selectedWidget">
70+
<option value="checkbox">Checkbox</option>
71+
<option value="toggle">Toggle</option>
72+
</select>
73+
{/if}
74+
<div>
75+
<button>Submit</button>
76+
</div>
77+
</form>

0 commit comments

Comments
 (0)