Skip to content

Commit 076bf4b

Browse files
committed
Discriminated unions for the form itself weren't including the union keys for the schema, when parsing the form data.
1 parent fbdfdee commit 076bf4b

File tree

7 files changed

+197
-10
lines changed

7 files changed

+197
-10
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
- `FormPath` now extends only basic objects and arrays, avoiding issues with classes, built-in objects like `File` and `Date`, and special "branded" types that validation libraries are using. Thanks to [Matt DeKok](https://github.com/sillvva) for this fix!
1313
- [SuperDebug](https://superforms.rocks/super-debug) always renders left-to-right now.
14+
- Discriminated unions for the form itself weren't including the union keys for the schema, when parsing the form data.
1415

1516
## [2.13.0] - 2024-05-03
1617

src/lib/formData.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,32 @@ function _parseFormData<T extends Record<string, unknown>>(
153153
options?: SuperValidateOptions<T>
154154
) {
155155
const output: Record<string, unknown> = {};
156-
const schemaKeys = options?.strict
157-
? new Set([...formData.keys()].filter((key) => !key.startsWith('__superform_')))
158-
: new Set(
159-
[
160-
...Object.keys(schema.properties ?? {}),
161-
...(schema.additionalProperties ? formData.keys() : [])
162-
].filter((key) => !key.startsWith('__superform_'))
163-
);
156+
157+
let schemaKeys: Set<string>;
158+
159+
if (options?.strict) {
160+
schemaKeys = new Set([...formData.keys()].filter((key) => !key.startsWith('__superform_')));
161+
} else {
162+
let unionKeys: string[] = [];
163+
164+
// Special fix for union schemas, then the keys must be gathered from the objects in the union
165+
if (schema.anyOf) {
166+
const info = schemaInfo(schema, false, []);
167+
if (info.union?.some((s) => s.type !== 'object')) {
168+
throw new SchemaError('All form types must be an object if schema is a union.');
169+
}
170+
171+
unionKeys = info.union?.flatMap((s) => Object.keys(s.properties ?? {})) ?? [];
172+
}
173+
174+
schemaKeys = new Set(
175+
[
176+
...unionKeys,
177+
...Object.keys(schema.properties ?? {}),
178+
...(schema.additionalProperties ? formData.keys() : [])
179+
].filter((key) => !key.startsWith('__superform_'))
180+
);
181+
}
164182

165183
function parseSingleEntry(key: string, entry: FormDataEntryValue, info: SchemaInfo) {
166184
if (options?.preprocessed && options.preprocessed.includes(key as keyof T)) {
@@ -243,6 +261,8 @@ function parseFormDataEntry(
243261
type: Exclude<SchemaType, 'null'>,
244262
info: SchemaInfo
245263
): unknown {
264+
//console.log(`Parsing FormData ${key} (${type}): "${value}"`, info); //debug
265+
246266
if (!value) {
247267
//console.log(`No FormData for "${key}" (${type}).`, info); //debug
248268

src/routes/(v2)/v2/Navigation.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
'form-result-type',
6060
'nested-traverse',
6161
'reset-errors',
62-
'schemasafe-types'
62+
'schemasafe-types',
63+
'discriminated-union'
6364
].sort();
6465
</script>
6566

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { superValidate, message } from '$lib/index.js';
2+
import { zod } from '$lib/adapters/zod.js';
3+
import { fail } from '@sveltejs/kit';
4+
import { schema } from './schema.js';
5+
6+
export const load = async () => {
7+
const adapter = zod(schema);
8+
console.dir(adapter.jsonSchema, { depth: 10 }); //debug
9+
const form = await superValidate(adapter);
10+
return { form };
11+
};
12+
13+
export const actions = {
14+
default: async ({ request }) => {
15+
const formdata = await request.formData();
16+
const form = await superValidate(formdata, zod(schema));
17+
console.log(form, formdata);
18+
19+
if (!form.valid) return fail(400, { form });
20+
21+
return message(form, 'Form posted successfully!');
22+
}
23+
};
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<script lang="ts">
2+
import { page } from '$app/stores';
3+
import { superForm } from '$lib/index.js';
4+
import SuperDebug from '$lib/index.js';
5+
6+
export let data;
7+
8+
const { form, errors, message, enhance } = superForm(data.form, {
9+
taintedMessage: null,
10+
resetForm: true
11+
});
12+
13+
// eslint-disable-next-line svelte/valid-compile
14+
$page;
15+
</script>
16+
17+
<SuperDebug data={$form} />
18+
19+
<h3>Superforms testing ground - Zod</h3>
20+
21+
{#if $message}
22+
<div class="status" class:error={$page.status >= 400} class:success={$page.status == 200}>
23+
{$message}
24+
</div>
25+
{/if}
26+
27+
<form method="POST" use:enhance>
28+
<label>
29+
Type<br />
30+
<input
31+
name="type"
32+
type="text"
33+
aria-invalid={$errors.type ? 'true' : undefined}
34+
bind:value={$form.type}
35+
/>
36+
{#if $errors.type}<span class="invalid">{$errors.type}</span>{/if}
37+
</label>
38+
39+
<button>Submit</button>
40+
</form>
41+
42+
<hr />
43+
<p><a target="_blank" href="https://superforms.rocks/api">API Reference</a></p>
44+
45+
<style>
46+
.invalid {
47+
color: red;
48+
}
49+
50+
.status {
51+
color: white;
52+
padding: 4px;
53+
padding-left: 8px;
54+
border-radius: 2px;
55+
font-weight: 500;
56+
}
57+
58+
.status.success {
59+
background-color: seagreen;
60+
}
61+
62+
.status.error {
63+
background-color: #ff2a02;
64+
}
65+
66+
input {
67+
background-color: #ddd;
68+
}
69+
70+
a {
71+
text-decoration: underline;
72+
}
73+
74+
hr {
75+
margin-top: 4rem;
76+
}
77+
78+
form {
79+
padding-top: 1rem;
80+
padding-bottom: 1rem;
81+
}
82+
</style>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { z } from 'zod';
2+
3+
export const schema = z.discriminatedUnion('type', [
4+
z.object({
5+
type: z.literal('empty')
6+
}),
7+
z.object({
8+
type: z.literal('extra'),
9+
roleId: z.string()
10+
})
11+
]);

src/tests/superValidate.test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,6 @@ describe('Schemasafe', () => {
301301

302302
const adapter = schemasafe(schema);
303303
const form = await superValidate(request, adapter);
304-
console.log('🚀 ~ it ~ form:', form);
305304
const email: string = form.data.email;
306305

307306
assert(!form.valid);
@@ -1110,6 +1109,56 @@ describe('Array validation', () => {
11101109
});
11111110
});
11121111

1112+
describe('Top-level union', () => {
1113+
it('should handle unions on the top-level', async () => {
1114+
/*
1115+
const schema: JSONSchema7 = {
1116+
anyOf: [
1117+
{
1118+
type: 'object',
1119+
properties: { type: { type: 'string', const: 'empty' } },
1120+
required: ['type'],
1121+
additionalProperties: false
1122+
},
1123+
{
1124+
type: 'object',
1125+
properties: {
1126+
type: { type: 'string', const: 'extra' },
1127+
roleId: { type: 'string' }
1128+
},
1129+
required: ['type', 'roleId'],
1130+
additionalProperties: false
1131+
}
1132+
],
1133+
$schema: 'http://json-schema.org/draft-07/schema#'
1134+
};
1135+
*/
1136+
1137+
const schema = z.discriminatedUnion('type', [
1138+
z.object({
1139+
type: z.literal('empty')
1140+
}),
1141+
z.object({
1142+
type: z.literal('extra'),
1143+
roleId: z.string()
1144+
})
1145+
]);
1146+
1147+
const formData = new FormData();
1148+
formData.set('type', 'extra');
1149+
formData.set('roleId', 'ABC');
1150+
1151+
const form = await superValidate(formData, zod(schema));
1152+
//console.log('🚀 ~ it ~ form:', form);
1153+
1154+
assert(form.valid);
1155+
assert(form.data.type == 'extra');
1156+
1157+
const roleId: string = form.data.roleId;
1158+
expect(roleId).toBe('ABC');
1159+
});
1160+
});
1161+
11131162
describe('Enum validation', () => {
11141163
const fishes = ['trout', 'tuna', 'shark'] as const;
11151164

0 commit comments

Comments
 (0)