Skip to content

Commit 9048a23

Browse files
committedApr 15, 2023
fix: handle zod unions closes #4204
1 parent 65687d9 commit 9048a23

File tree

3 files changed

+120
-35
lines changed

3 files changed

+120
-35
lines changed
 

‎.changeset/purple-mayflies-share.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@vee-validate/i18n': patch
3+
'@vee-validate/rules': patch
4+
'vee-validate': patch
5+
'@vee-validate/yup': patch
6+
'@vee-validate/zod': patch
7+
---
8+
9+
fixed zod union issues not showing up as errors closes #4204

‎packages/zod/src/index.ts

+29-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ZodObject, input, output, ZodDefault, ZodSchema, ParseParams } from 'zod';
1+
import { ZodObject, input, output, ZodDefault, ZodSchema, ParseParams, ZodIssue } from 'zod';
22
import { PartialDeep } from 'type-fest';
33
import type { TypedSchema, TypedSchemaError } from 'vee-validate';
44
import { isIndex, isObject, merge } from '../../shared';
@@ -22,16 +22,8 @@ export function toTypedSchema<
2222
};
2323
}
2424

25-
const errors: Record<string, TypedSchemaError> = result.error.issues.reduce((acc, issue) => {
26-
const path = joinPath(issue.path);
27-
if (!acc[path]) {
28-
acc[path] = { errors: [], path };
29-
}
30-
31-
acc[path].errors.push(issue.message);
32-
33-
return acc;
34-
}, {} as Record<string, TypedSchemaError>);
25+
const errors: Record<string, TypedSchemaError> = {};
26+
processIssues(result.error.issues, errors);
3527

3628
return {
3729
errors: Object.values(errors),
@@ -55,10 +47,36 @@ export function toTypedSchema<
5547
return schema;
5648
}
5749

50+
function processIssues(issues: ZodIssue[], errors: Record<string, TypedSchemaError>): void {
51+
issues.forEach(issue => {
52+
const path = joinPath(issue.path);
53+
if (issue.code === 'invalid_union') {
54+
processIssues(
55+
issue.unionErrors.flatMap(ue => ue.issues),
56+
errors
57+
);
58+
59+
if (!path) {
60+
return;
61+
}
62+
}
63+
64+
if (!errors[path]) {
65+
errors[path] = { errors: [], path };
66+
}
67+
68+
errors[path].errors.push(issue.message);
69+
});
70+
}
71+
5872
/**
5973
* Constructs a path with brackets to be compatible with vee-validate path syntax
6074
*/
6175
function joinPath(path: (string | number)[]): string {
76+
if (!path.length) {
77+
return '';
78+
}
79+
6280
let fullPath = String(path[0]);
6381
for (let i = 1; i < path.length; i++) {
6482
if (isIndex(path[i])) {

‎packages/zod/tests/zod.spec.ts

+82-24
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import { useField, useForm } from '@/vee-validate';
22
import { toTypedSchema } from '@/zod';
33
import { mountWithHoc, flushPromises, setValue } from 'vee-validate/tests/helpers';
44
import { Ref } from 'vue';
5-
import * as zod from 'zod';
5+
import { z } from 'zod';
66

77
const REQUIRED_MSG = 'field is required';
88
const MIN_MSG = 'field is too short';
99
const EMAIL_MSG = 'field must be a valid email';
1010

11-
test('validates typed field with yup', async () => {
11+
test('validates typed field with zod', async () => {
1212
const wrapper = mountWithHoc({
1313
setup() {
14-
const rules = toTypedSchema(zod.string().min(1, REQUIRED_MSG).min(8, MIN_MSG));
14+
const rules = toTypedSchema(z.string().min(1, REQUIRED_MSG).min(8, MIN_MSG));
1515
const { value, errorMessage } = useField('test', rules);
1616

1717
return {
@@ -45,7 +45,7 @@ test('generates multiple errors for any given field', async () => {
4545
let errors!: Ref<string[]>;
4646
const wrapper = mountWithHoc({
4747
setup() {
48-
const rules = toTypedSchema(zod.string().min(1, REQUIRED_MSG).min(8, MIN_MSG));
48+
const rules = toTypedSchema(z.string().min(1, REQUIRED_MSG).min(8, MIN_MSG));
4949
const { value, errors: fieldErrors } = useField('test', rules);
5050

5151
errors = fieldErrors;
@@ -72,9 +72,9 @@ test('shows multiple errors using error bag', async () => {
7272
const wrapper = mountWithHoc({
7373
setup() {
7474
const schema = toTypedSchema(
75-
zod.object({
76-
email: zod.string().email(EMAIL_MSG).min(7, MIN_MSG),
77-
password: zod.string().min(8, MIN_MSG),
75+
z.object({
76+
email: z.string().email(EMAIL_MSG).min(7, MIN_MSG),
77+
password: z.string().min(8, MIN_MSG),
7878
})
7979
);
8080

@@ -125,13 +125,13 @@ test('shows multiple errors using error bag', async () => {
125125
expect(passwordError.textContent).toBe('');
126126
});
127127

128-
test('validates typed schema form with yup', async () => {
128+
test('validates typed schema form with zod', async () => {
129129
const wrapper = mountWithHoc({
130130
setup() {
131131
const schema = toTypedSchema(
132-
zod.object({
133-
email: zod.string().email(EMAIL_MSG).min(1, REQUIRED_MSG),
134-
password: zod.string().min(8, MIN_MSG),
132+
z.object({
133+
email: z.string().email(EMAIL_MSG).min(1, REQUIRED_MSG),
134+
password: z.string().min(8, MIN_MSG),
135135
})
136136
);
137137

@@ -182,13 +182,71 @@ test('validates typed schema form with yup', async () => {
182182
expect(passwordError.textContent).toBe('');
183183
});
184184

185+
// #4204
186+
test('handles zod union errors', async () => {
187+
const wrapper = mountWithHoc({
188+
setup() {
189+
const schema = z.object({
190+
email: z.string().email({ message: 'valid email' }).min(1, 'Email is required'),
191+
name: z.string().min(1, 'Name is required'),
192+
});
193+
194+
const schemaBothUndefined = z.object({
195+
email: z.undefined(),
196+
name: z.undefined(),
197+
});
198+
199+
const bothOrNeither = schema.or(schemaBothUndefined);
200+
201+
const { useFieldModel, errors } = useForm({
202+
validationSchema: toTypedSchema(bothOrNeither),
203+
});
204+
205+
const [email, name] = useFieldModel(['email', 'name']);
206+
207+
return {
208+
schema,
209+
email,
210+
name,
211+
errors,
212+
};
213+
},
214+
template: `
215+
<div>
216+
<input id="email" name="email" v-model="email" />
217+
<span id="emailErr">{{ errors.email }}</span>
218+
219+
<input id="name" name="name" v-model="name" />
220+
<span id="nameErr">{{ errors.name }}</span>
221+
</div>
222+
`,
223+
});
224+
225+
const email = wrapper.$el.querySelector('#email');
226+
const name = wrapper.$el.querySelector('#name');
227+
const emailError = wrapper.$el.querySelector('#emailErr');
228+
const nameError = wrapper.$el.querySelector('#nameErr');
229+
230+
await flushPromises();
231+
232+
setValue(name, '4');
233+
await flushPromises();
234+
expect(nameError.textContent).toBe('Expected undefined, received string');
235+
236+
setValue(email, 'test@gmail.com');
237+
await flushPromises();
238+
239+
expect(emailError.textContent).toBe('');
240+
expect(nameError.textContent).toBe('');
241+
});
242+
185243
test('uses zod for form values transformations and parsing', async () => {
186244
const submitSpy = vi.fn();
187245
mountWithHoc({
188246
setup() {
189247
const schema = toTypedSchema(
190-
zod.object({
191-
age: zod.preprocess(arg => Number(arg), zod.number()),
248+
z.object({
249+
age: z.preprocess(arg => Number(arg), z.number()),
192250
})
193251
);
194252

@@ -223,8 +281,8 @@ test('uses zod default values for submission', async () => {
223281
mountWithHoc({
224282
setup() {
225283
const schema = toTypedSchema(
226-
zod.object({
227-
age: zod.number().default(11),
284+
z.object({
285+
age: z.number().default(11),
228286
})
229287
);
230288

@@ -257,13 +315,13 @@ test('uses zod default values for initial values', async () => {
257315
mountWithHoc({
258316
setup() {
259317
const schema = toTypedSchema(
260-
zod.object({
261-
name: zod.string().default('test'),
262-
age: zod.number().default(11),
263-
unknownKey: zod.string(),
264-
object: zod.object({
265-
nestedKey: zod.string(),
266-
nestedDefault: zod.string().default('nested'),
318+
z.object({
319+
name: z.string().default('test'),
320+
age: z.number().default(11),
321+
unknownKey: z.string(),
322+
object: z.object({
323+
nestedKey: z.string(),
324+
nestedDefault: z.string().default('nested'),
267325
}),
268326
})
269327
);
@@ -301,8 +359,8 @@ test('default values should not be undefined', async () => {
301359
mountWithHoc({
302360
setup() {
303361
const schema = toTypedSchema(
304-
zod.object({
305-
email: zod.string().min(1),
362+
z.object({
363+
email: z.string().min(1),
306364
})
307365
);
308366

0 commit comments

Comments
 (0)
Please sign in to comment.