99

Concurrent visitors over the last 30 minutes. (~)

Peak concurrent visitors -
Yesterday: 0 / Today: 0

Zod Essentials - Runtime Type Validation with TypeScript

Zod Essentials - Runtime Type Validation with TypeScript

This post is based on Zod version 4.1.5.

TypeScript gives you strong type checking at compile time, but it can't guarantee type safety at runtime.
Zod is a library that validates data at runtime, and using it together with TypeScript's compile-time type checking gives you stronger type safety.
Of course, you can also use it in JavaScript.

# Installation and basic usage

Install it like this.

BASH
1
npm i zod

After installing, prepare a user object for a quick test like this.

/user.ts
TS
1
2
3
4
5
// User data export const userData = { name: 'Neo', age: 22 }

Next, define a schema to validate the user data.
The z object in Zod gives you many schema methods, such as .object(), .string(), and .number().
Once you have defined the data schema you want, call a method such as .parse() on that schema and pass the user data as an argument.
Zod then checks (validates) at runtime whether the user data matches the schema you defined.
If the data doesn't match the schema, an error is thrown at runtime.

/main.js
JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { z } from 'zod' import { userData } from './user.js' // Define the schema const userSchema = z.object({ name: z.string(), age: z.number() }) // Validate the data and handle errors try { userSchema.parse(userData) } catch (error) { if (error instanceof z.ZodError) { console.error(error.issues) } }
JS code

If you use TypeScript, you may need the type of the schema you defined.
You can infer the type with z.infer and get the schema's type as the result.
And with TypeScript's satisfies keyword, you can easily check whether the data satisfies that type.

/main.ts
TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { z } from 'zod' import { userData } from './user' // Define the schema and type const userSchema = z.object({ name: z.string(), age: z.number() }) type UserSchema = z.infer<typeof userSchema> // Validate the data and handle errors try { userSchema.parse(userData satisfies UserSchema) // Check that the data satisfies the type } catch (error) { if (error instanceof z.ZodError) { console.error(error.issues) // [...] } }
TS code

# Schema validation

With the validation methods available on a schema object, you can check whether the given data matches the schema.

The data that validation methods such as .parse() and .safeParse() (covered below) return on success is a deep copy.
If the immutability of nested reference types matters to you, it's better not to use the returned data (the copy).

# .parse()

The .parse() method validates data, returns the data if validation succeeds, and throws an error if it fails.
So you may need to handle errors with a try/catch statement.
When an error occurs, you can read the issues array property of the error object to see various error details.

We look at errors in more detail in the Error handling part.

/main.ts
TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { z } from 'zod' import { userData } from './user' // Define const userSchema = z.object({ name: z.string(), age: z.number() }) type User = z.infer<typeof userSchema> try { // Validate const result = userSchema.parse(userData satisfies User) // Success console.log(result) // { name: 'Neo', age: 22 } } catch (error) { // Failure if (error instanceof z.ZodError) { console.error(error.issues) // [...] } }

# .safeParse()

The .safeParse() method validates data and returns a result object that contains success or failure information.
On success, you read the data from the data property of the result (result), and on failure, you handle the error information through the error property.
Because .safeParse() doesn't throw an error even when validation fails, you don't need a try/catch statement.

/main.ts
TS
1
2
3
4
5
6
7
8
9
10
11
12
// ... // Validate const result = userSchema.safeParse(userData satisfies User) // Success console.log(result.data) // { name: 'Neo', age: 22 } // Failure if (result.error instanceof z.ZodError) { console.error(error.issues) // [...] }

# .parseAsync()

The .parseAsync() method is similar to the .parse() method we saw earlier, but it validates asynchronously with the await keyword.
Use it when your schema definition needs asynchronous processing.

In the next example, the isNameExists function asynchronously checks whether a user name already exists.
It simply returns whether the name exists after 1 second.
You can also test it by setting the argument of the resolve call to true/false.

/user.ts
TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// User data export const userData = { name: 'Neo', age: 22 } // Example async function that checks whether a name exists export function isNameExists(name: string) { return new Promise(resolve => { // fetch(`https://api.heropy.dev/v0/users/exists?name=${name}`) // .then(res => res.json()) // .then(name => setTimeout(() => resolve(Boolean(name)), 1000)) setTimeout(() => resolve(Boolean(name)), 1000) }) }

In the next example, we validate that the user name (name) is a string, and also use the isNameExists function to check whether the name already exists.
Because the isNameExists function returns a Promise instance, you need the parseAsync method to wait for the validation.

When you define a schema, the refine method lets you add a callback function for custom validation.
The callback function must return a boolean.

/main.ts
TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { z } from 'zod' import { userData, isNameExists } from './user' const userSchema = z.object({ name: z .string() .refine( val => isNameExists(val), { error: 'This name is already taken.' } ), age: z.number() }) type User = z.infer<typeof userSchema> try { // Validate const res = await userSchema.parseAsync(userData satisfies User) // Success console.log(res) // { name: 'Neo', age: 22 } } catch (error) { // Failure if (error instanceof z.ZodError) { console.error(error.issues[0]) // { code: 'custom', path: ['name'], message: 'This name is already taken.' } } }

# .safeParseAsync()

Like the .safeParse() method we saw earlier, the .safeParseAsync() method returns a result object without a try/catch statement, and like .parseAsync(), you call it asynchronously with the await keyword.

/main.ts
TS
1
2
3
4
5
6
7
8
9
10
11
12
// ... // Validate const result = await userSchema.safeParseAsync(userData satisfies User) // Success console.log(result.data) // { name: 'Neo', age: 22 } // Failure if (result.error instanceof z.ZodError) { console.error(result.error.issues[0]) // { code: 'custom', path: ['name'], message: 'This name is already taken.' } }

# Defining schemas

To validate data, you first need to define a schema.
A schema can represent many types, from simple primitives to complex nested objects.

# String types

TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// String z.string() z.string().min(2) // Minimum length z.string().max(10) // Maximum length z.string().length(7) // Exact length z.string().regex(/^[a-z]+$/) // Regular expression z.string().startsWith('A') // Starts with the string z.string().endsWith('Z') // Ends with the string z.string().includes('abc') // Contains the string z.string().uppercase() // Uppercase z.string().lowercase() // Lowercase // Template literal z.templateLiteral(['Hello ', z.string().min(8), '?!']) // => `Hello ${'template'}?!` // => `Hello ${'world'}?!` // ❌ // Email z.email() // => 'thesecon@gmail.com' z.email({ pattern: /^\w+@\w+\.\w+$/ }) // Regular expression z.email({ pattern: z.regexes.email }) // Default email z.email({ pattern: z.regexes.html5Email }) // HTML5 email z.email({ pattern: z.regexes.rfc5322Email }) // RFC 5322 email z.email({ pattern: z.regexes.unicodeEmail }) // Unicode email // URL z.url() // => 'https://heropy.dev', 'http://localhost:3000', 'mailto:thesecon@gmail.com' // Hostname z.hostname() // => 'example.com', 'localhost', '127.0.0.1' // Emoji z.emoji() // => '👍' // Base64 z.base64() // Standard version z.base64url() // URL-safe version // => 'SGVsbG8gd29ybGQ=' // => 'data:text/plain;base64,SGVsbG8gd29ybGQ=' // ❌ // JSON Web Token z.jwt() // => 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.e30.' // UUID z.uuid() z.uuid({ version: 'v4' }) // Specify the version, v1-8 z.uuidv4() // v4 z.uuidv6() // v6 z.uuidv7() // v7 // => '123e4567-e89b-12d3-a456-426614174000' // Random string (21+ characters) z.nanoid() // => 'abcdefghij12345678901' // Collision-resistant ID (9+ characters) z.cuid() // => 'c12345678' // 128-bit ID (26 characters) z.ulid() // => '01H7Z8K4P2M9N3Q59V0W1X2Y3Z' // IPv4, IPv6 z.ipv4() // => '192.168.1.1' z.ipv6() // => '2001:0db8:85a3:0000:0000:8a2e:0370:7334' z.cidrv4() // => '192.168.1.0/24' z.cidrv6() // => '2001:0db8:85a3:0000:0000:8a2e:0370:7334/64' // Date z.date() // => new Date('2025-12-16') z.date().min(new Date('2025-01-01')).max(new Date('2025-12-31')) // Limit the date range // => new Date('2025-12-16') // => new Date('2026-04-03') // ❌ z.iso.date() // ISO 8601 date z.iso.time() // ISO 8601 time z.iso.datetime() // ISO 8601 date and time z.iso.duration() // ISO 8601 duration (period) // =>'2025-04-03' // =>'12:00', '12:00:00', '12:00:00.000' // =>'2025-04-03T12:00:00Z' // =>'P1Y2M3DT4H5M6S'

# Number types

TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Number z.number() z.number().gt(5) // Greater than z.number().gte(5) // Greater than or equal to z.number().min(5) // Minimum, same as `gte` z.number().lt(5) // Less than z.number().lte(5) // Less than or equal to z.number().max(5) // Maximum, same as `lte` z.number().positive() // Positive z.number().nonpositive() // Not positive z.number().negative() // Negative z.number().nonnegative() // Not negative z.number().multipleOf(2) // Multiple of z.number().step(2) // Multiple of, same as `multipleOf` // Safe integer z.int() // => ~9007199254740991 // 32-bit integer z.int32() // => ~2147483647 // Big integer z.bigint() // => 1234n z.int64() // 64-bit integer, same as `bigint` // Floating point z.float32() // 32-bit single precision, about 7 digits z.float64() // 64-bit double precision, about 15 digits // Not a Number z.nan() // => NaN

# Other primitive types

TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Boolean z.boolean() // String boolean const stb1 = z.stringbool() stb1.parse('true') // true stb1.parse('1') // true stb1.parse('yes') // true stb1.parse('false') // false stb1.parse('0') // false // The defaults of `stringbool` are as follows z.stringbool({ truthy: ['true', '1', 'yes', 'on', 'y', 'enabled'], // Strings treated as true falsy: ['false', '0', 'no', 'off', 'n', 'disabled'], // Strings treated as false case: 'insensitive' // Case-insensitive }) const stb2 = z.stringbool({ truthy: ['Yes', 'Y'], falsy: ['No', 'N'], case: 'sensitive' // Case-sensitive }) // Custom stb2.parse('Yes') // true stb2.parse('yes') // true stb2.parse('N') // false // Symbol z.symbol() // => Symbol('id') // Null, Undefined z.null() // => null z.undefined() // => undefined z.void() // => void, same as undefined // Can be undefined - z.optional(schema) z.optional(z.number()) // => 123, undefined // => null // ❌ // Can be null - z.nullable(schema) z.nullable(z.number()) // => 123, null // => undefined // ❌ // Can be null or undefined - z.nullish(schema) z.nullish(z.number()) // => 123, null, undefined

# Reference types

TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// Object - strips unknown properties - z.object(object_schema) const obj1 = z.object({ name: z.string(), age: z.number() }) obj1.parse({ name: 'Neo' }) // ❌ obj1.parse({ name: 'Neo', age: 22 }) // { name: 'Neo', age: 22 } obj1.parse({ name: 'Neo', age: 22, isValid: true }) // { name: 'Neo', age: 22 }, the isValid property is removed // Object - errors on unknown properties - z.strictObject(object_schema) const obj2 = z.strictObject({ name: z.string(), age: z.number() }) obj2.parse({ name: 'Neo' }) // ❌ obj2.parse({ name: 'Neo', age: 22 }) // { name: 'Neo', age: 22 } obj2.parse({ name: 'Neo', age: 22, isValid: true }) // ❌ // Object - allows unknown properties - z.looseObject(object_schema) const obj3 = z.looseObject({ name: z.string(), age: z.number() }) obj3.parse({ name: 'Neo' }) // ❌ obj3.parse({ name: 'Neo', age: 22 }) // { name: 'Neo', age: 22 } obj3.parse({ name: 'Neo', age: 22, isValid: true }) // { name: 'Neo', age: 22, isValid: true }, the isValid property is kept // Object methods const obj4 = z.object({ name: z.string(), age: z.number().optional() // Defined as an optional property }) obj4.catchAll(z.boolean()) // Schema for the unknown extra properties you want to allow // => { isValid: z.boolean(), hasEmail: z.boolean() } obj4.shape.age // Schema of a property // z.number().optional() obj4.keyof() // Schema (enum) of all property names of the object // z.enum(['name', 'age']) obj4.extend({ isValid: z.boolean() }) // New merged object schema // z.object({ name: z.string(), age: z.number().optional(), isValid: z.boolean() }) obj4.pick({ name: true }) // Object schema with only the picked properties // z.object({ name: z.string() }) obj4.omit({ name: true }) // Object schema without the omitted properties // z.object({ age: z.number().optional() }) obj4.partial() // New object schema where every property is optional // z.object({ name: z.string().optional(), age: z.number().optional() }) obj4.required() // New object schema where every property is required // z.object({ name: z.string(), age: z.number() }) // Object - key-value - z.record(key_schema, value_schema) z.record(z.string(), z.number()) // => { a: 1, b: 2, c: 3 } // => { a: '1', b: '2', c: '3' } // ❌ // => {} // ❌ // Restrict the key types z.record( z.union([z.string(), z.number(), z.symbol()]), z.number() ) // => { abc: 1, 123: 2, [Symbol('id')]: 3 } // Restrict the key names z.record( z.enum(['name', 'age']), z.number() ) // => { name: 1, age: 2 } // => { name: 1, age: 2, isValid: 3 } // ❌ // Object - key-value with optional properties - z.partialRecord(key_schema, value_schema) z.partialRecord(z.string(), z.number()) // => {} // => { a: 1 } // => { a: 1, b: 2 } // => { a: 1, b: 2, c: 3 } // Array - z.array(item_schema) z.array(z.string()) // => ['A', 'B', 'C', 'D'] z.array(z.number()) // => [1, 2, 3, 4] z.array(z.union([z.string(), z.number()])) // => ['A', 1, 'B', 2, 'C', 3, 'D', 4] z.array(z.string()).min(3) // Minimum number of items z.array(z.string()).max(2) // Maximum number of items z.array(z.number()).unwrap() // Get the item schema of the array // Tuple - z.tuple(schema_array) z.tuple([z.string(), z.number()]) // => ['Neo', 22]

# Other types

TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// Literal - z.literal(value) z.literal('Hello') z.literal(123) z.literal(true) // Any type z.any() // Unknown type z.unknown() // Impossible type z.never() // Enum - z.enum(string_array) z.enum(['a', 'b']) // => 'a' // => 'b' // => 'c' // ❌ // Union - z.union(schema_array) z.union([z.string(), z.number()]) // => 'Hello' // => 123 // => true // ❌ // Discriminated union - z.discriminatedUnion(discriminator_key, schema_array) const dis1 = z.discriminatedUnion('status', [ z.object({ status: z.literal('success'), data: z.string() }), z.object({ status: z.literal('failed'), error: z.string() }) ]) type DU = z.infer<typeof dis1> // The DU type is as follows // { status: 'success'; data: string } | { status: 'failed'; error: string } function getData(res: DU) { dis1.parse(res) return res.status === 'success' ? res.data : res.error } getData({ status: 'success', data: 'Hello~' }) // 'Hello~' getData({ status: 'failed', error: 'Error~' }) // 'Error~' // Intersection - z.intersection(schema_array) const obj1 = z.object({ status: z.string(), data: z.string() }) const obj2 = z.object({ status: z.string(), error: z.string() }) const int1 = z.intersection(obj1, obj2) int1.parse({ status: 'success', data: 'Hello!', error: 'Error!' }) // JSON z.json() // => 'Hello' // => 123 // => { name: 'Neo', age: 22 } // => undefined // ❌ // => function () {} // ❌ // => new Date() // ❌

# Class types

TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class User { constructor(public name: string, public age: number) {} } // Instance - z.instanceof(class) const user = new User('Neo', 22) const date = new Date() const ins1 = z.instanceof(User) ins1.parse(user) ins1.parse(date) // ❌ // Property - z.property(property_name, schema) const pro1 = z.instanceof(User).check(z.property('age', z.number().min(18))) pro1.parse(new User('Neo', 22)) pro1.parse(new User('Neo', 17)) // ❌ // Map - z.map(key_schema, value_schema) const map = new Map() map.set(200, 'OK') map.set(404, 'Not Found') map.set(500, 'Internal Server Error') const ma1 = z.map(z.number(), z.string()) ma1.parse(map) // Set - z.set(value_schema) const set = new Set() set.add({ x: 123 }) const se1 = z.set(z.object({ x: z.number() })) se1.parse(set) // File z.file() z.file().min(1024) // Minimum size (bytes) z.file().max(1024) // Maximum size (bytes) z.file().mime('image/png') // MIME type const file = new File([], 'favicon.png') const fil1 = z.file() fil1.parse(file)

# Custom types

TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// Preprocess before the schema - z.preprocess(preprocess_function, schema) const pre1 = z.preprocess(val => { return typeof val === 'string' ? val.toUpperCase() : val }, z.string()) pre1.parse('abc') // 'ABC' pre1.parse(123) // ❌ // Custom schema - z.custom(validation_function) const cus1 = z.custom(val => { return typeof val === 'string' ? /[A-Z]/.test(val) : false }) cus1.parse('ABC') // 'ABC' cus1.parse(123) // ❌ // Transform - z.transform(transform_function) const tra1 = z.transform(val => { return typeof val === 'string' ? val.toUpperCase() : val }) tra1.parse('abc') // 'ABC' tra1.parse(123) // 123 // schema.transform(transform_function) const tra2 = z.string().transform(val => val.toUpperCase()) tra2.parse('abc') // 'ABC' tra2.parse(123) // ❌ // Connect schemas - z.pipe(in_schema, out_schema) const pip1 = z.pipe( z.string(), z.transform(val => val.length) ) pip1.parse('abc') // 3 pip1.parse(123) // ❌ // in_schema.pipe(out_schema) const pip2 = z.string().pipe(z.transform(val => val.length)) pip2.parse('abc') // 3 pip2.parse(123) // ❌ // Refine a schema - schema.refine(refine_function, options) const ref1 = z .string() .refine(val => val.length < 4, { error: 'Too many letters!' }) .refine(val => val.toUpperCase().includes('A'), { error: `Must include the letter 'A' or 'a'!` }) ref1.parse('abc') // 'abc' ref1.parse('xyz123') // ❌ // Refine a schema - schema.superRefine(refine_function) const sup1 = z.string().superRefine((val, ctx) => { if (val.length > 3) { ctx.addIssue({ code: 'custom', message: 'Too many letters!' }) } if (!val.toUpperCase().includes('A')) { ctx.addIssue({ code: 'custom', message: `must include the letter 'A' or 'a'.` }) } }) sup1.parse('abc') // 'abc' sup1.parse('xyz123') // ❌ // Check a schema - schema.check(check_function) const che1 = z.string().check(ctx => { if (ctx.value.length > 3) { ctx.issues.push({ code: 'custom', input: ctx.value, message: 'Too many letters!' }) } if (!ctx.value.toUpperCase().includes('A')) { ctx.issues.push({ code: 'custom', input: ctx.value, message: `must include the letter 'A' or 'a'.` }) } }) che1.parse('abc') // 'abc' che1.parse('xyz123') // ❌ // Default value - schema.default(default_value) const def1 = z .number() .transform(val => val + 10) .default(0) def1.parse(2) // 12 def1.parse(undefined) // 0 // Default value that goes through parsing - schema.prefault(default_value) const pre1 = z .number() .transform(val => val + 10) .prefault(0) pre1.parse(2) // 12 pre1.parse(undefined) // 10 // Fallback value on error const cat1 = z.number().catch(0) cat1.parse('Hello~') // 0 const cat2 = z.number().catch(ctx => { console.log(ctx.error.issues[0].message) // 'Invalid input: expected number, received string' return 0 }) cat2.parse('Hello~') // 0 // Readonly const arr = z.array(z.number()).readonly() const res = schema.parse([1, 2, 3]) res // [1, 2, 3] res.push(4) // ❌ // Error! ts(2339) // Branded type const id = z.email().brand<'ID'>() const pw = z.string().min(8).max(20).brand<'PW'>() type Id = z.infer<typeof id> type Pw = z.infer<typeof pw> const res1: Pw = id.parse('thesecon@gmail.com') // Error! ts(2322) const res2: Id = pw.parse('qwer1234') // Error! ts(2322) res1 // 'thesecon@gmail.com' res2 // 'qwer1234'

# Error handling

As we saw above, when you use a validation method such as .parse() or .safeParse() and the data fails validation, you can inspect the error.
The error object of a failed result is an instance of z.ZodError, and its issues property holds several issue objects as array items.
An issue object has properties such as expected and code, and in particular, its message property tells you why the error happened.

TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// .parse() + try/catch try { z.number().parse('Hello~') // ❌ } catch (error) { if (error instanceof z.ZodError) { console.log(error.issues) } } // .safeParse() const result = z.number().safeParse('Hello~') // ❌ if (result.error instanceof z.ZodError) { console.log(result.error.issues) } // Example issue object structure, error.issues[0] // => { // expected: 'number', // Expected type // code: 'invalid_type', // Error code // path: ['age'], // Path (property) where the error occurred // message: 'Invalid input: expected number, received string' // Error message // }

Zod supports internationalization.
So you can also print the default error messages in Korean.

You can also add your own error messages directly to a schema or to a validation method.
Note that a custom error message on the schema takes precedence over one on the validation method.

TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { z } from 'zod' import { ko } from 'zod/locales' z.config(ko()) // Korean locale try { z.number().parse('Hello~') // '잘못된 입력: 예상 타입은 number, 받은 타입은 string입니다' (Korean for 'Invalid input: expected number, received string') z.number('High priority').parse('Hello~') z.number({ error: 'High priority' }).parse('Hello~') z.number({ error: issue => 'High priority' }).parse('Hello~') z.number({ error: issue => 'High priority' }).parse('Hello~', { error: issue => 'Low priority' }) // 'High priority' z.number().parse('Hello~', { error: issue => 'Low priority' }) // 'Low priority' } catch (error) { if (error instanceof z.ZodError) { console.log(error.issues[0].message) } }

To display error information more flexibly, Zod provides utility methods such as .treeifyError().

Member Description
error.issues Shows the errors as an array
z.treeifyError(error) Shows the errors as a tree
z.prettifyError(error) Shows the errors as human-readable text
z.flattenError(error) Shows the errors in a clean, flat shape
TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import { z } from 'zod' import { en } from 'zod/locales' z.config(en()) try { z.strictObject({ name: z.string(), age: z.number() }).parse({ name: 123, age: 'Neo', isValid: true }) } catch (error) { if (error instanceof z.ZodError) { console.log(error.issues) // Shows the errors as an array // => [ // { // expected: 'string', // code: 'invalid_type', // path: [ 'name' ], // message: 'Invalid input: expected string, received number' // }, // { // expected: 'number', // code: 'invalid_type', // path: [ 'age' ], // message: 'Invalid input: expected number, received string' // }, // { // code: 'unrecognized_keys', // keys: [ 'isValid' ], // path: [], // message: 'Unrecognized key: "isValid"' // } // ] console.log(z.treeifyError(error)) // Shows the errors as a tree // => { // errors: [ 'Unrecognized key: "isValid"' ], // properties: { // name: { // errors: [ 'Invalid input: expected string, received number' ] // }, // age: { // errors: [ 'Invalid input: expected number, received string' ] // } // } // } console.log(z.prettifyError(error)) // Shows the errors as human-readable text // => ✖ Unrecognized key: 'isValid' // ✖ Invalid input: expected string, received number // → at name // ✖ Invalid input: expected number, received string // → at age console.log(z.flattenError(error)) // Shows the errors in a clean, flat shape // => { // formErrors: [ 'Unrecognized key: "isValid"' ], // fieldErrors: { // name: [ 'Invalid input: expected string, received number' ], // age: [ 'Invalid input: expected number, received string' ] // } // } } }

# Examples

# Validating a login form

The following example is based on Starting a React Project with Vite.

Output of the login form validation example

/src/App.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import { useState } from 'react' import { z } from 'zod' const schema = z.object({ id: z.email({ error: 'The email format is not valid.' }), pw: z.string().min(5, { error: issue => `The password must be at least ${issue.minimum} characters.` }) }) function TextField( props: React.InputHTMLAttributes<HTMLInputElement> & { error: string } ) { return ( <div className="mb-3"> <input className="w-full rounded-md border-2 border-gray-300 p-2" {...props} /> {props.error && ( <div className="mt-1 text-sm text-red-500">{props.error}</div> )} </div> ) } export default function App() { const [id, setId] = useState('') const [pw, setPw] = useState('') const [idError, setIdError] = useState('') const [pwError, setPwError] = useState('') function validateFields() { setIdError('') setPwError('') const result = schema.safeParse({ id, pw }) if (result.error instanceof z.ZodError) { result.error.issues.forEach(issue => { switch (issue.path[0]) { case 'id': setIdError(issue.message) break case 'pw': setPwError(issue.message) break } }) return false } return true } function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() if (!validateFields()) return // Handle the login.. console.log('Logged in!') } return ( <> <form className="max-w-md px-4 py-6" onSubmit={handleSubmit}> <TextField placeholder="Enter your email." type="text" value={id} onChange={e => setId(e.target.value)} error={idError} /> <TextField placeholder="Enter your password." type="password" value={pw} onChange={e => setPw(e.target.value)} error={pwError} /> <button className="w-full cursor-pointer rounded-md bg-blue-500 p-2 text-white hover:bg-blue-600" type="submit"> Log in </button> </form> </> ) }