[キャッチアップ] zod
Zod
公式ドキュメントを眺めながら広く浅く確認するだけのスクラップ
Introduction
- Zod は TS ファーストに宣言的にスキーマを定義し、バリデーションを実行できるライブラリ
- Zod の目的は型定義の重複を減らすことで、一度バリデーターを定義するだけで複雑な型定義も自動で行うことができる
Instalattion
TypeScript は 4.1 からサポートされ、strict
モードで TypeScript を使用することが必須になる。
{
// ...
"compilerOptions": {
// ...
"strict": true
}
}
インストールコマンド
npm install zod
Basic Usage
zod モジュールを用いてスキーマを定義し、それを満たしているかのバリデーションが可能
import { z } from "zod";
// User スキーマの作成
const User = z.object({
username: z.string(),
});
// スキーマに基づくバリデーション(成功例 {username: "John"})
const user1 = User.parse({
username: "John",
});
// スキーマに基づくバリデーション(失敗例 throws ZodError)
const user2 = User.parse({
username: 123,
});
スキーマから型を抜き出せる
// スキーマから型を抜き出す
type User = z.infer<typeof User> // { username: string }
Primitives
いつもの面々だけどランタイムでバリデートできるので、 bigint
や date
みたいなのものある。
z.string();
z.number();
z.bigint();
z.boolean();
z.date();
特殊な型もプリミティブとして用意されてる
z.undefined()
z.null()
z.void()
z.any()
z.unknown()
z.never()
Literals
TSファーストなのでリテラル型に対応するスキーマもある
z.literal('foo')
z.literal(12)
z.literal(true)
リテラルは値が確定してるので value
を取り出せる
z.literal('Hello').value // 'Hello'
String
zod はランタイムバリデータなので、型だけでなく値レベルでの制約を定義できる
z.string().max(5) // 最大5文字
z.string().min(5) // 最小5文字
z.string().length(5) // 5文字のみ
z.string().email() // メールアドレス
z.string().url() // URL
z.string().uuid() // UUID
z.string().regex(/^[a-z]+$/) // 正規表現
z.string().startsWith('foo') // 'foo'で始まる
z.string().endsWith('foo') // 'foo'で終わる
各スキーマでのバリデーションエラー時のメッセージはカスタムできる
z.string().max(5, { message: '最大5文字です' })
z.string({
required_error: '必須です',
invalid_type_error: '文字列ではありません',
}
Number
同様にランタイムバリデータを定義できる
z.number().gt(5) // 5より大きい
z.number().gte(5) // 5以上
z.number().lt(5) // 5より小さい
z.number().lte(5) // 5以下
z.number().positive() // 正の数
z.number().negative() // 負の数
z.number().nonpositive() // 0以下
z.number().nonnegative() // 0以上
z.number().int() // 整数
z.number().multipleOf(5) // 5の倍数
NaNs
NaN
であるというスキーマ。あんまり使い所はなさそうだけど。
z.nan({
invalid_type_error: "NaNではありません",
});
Booleans
z.boolean({
invalid_type_error: "真偽値ではありません",
})
Dates
日付のバリデーションも可能
z.date().min(new Date("2020-01-01")); // 2020-01-01以降
z.date().max(new Date("2020-01-01")); // 2020-01-01以前
Zod enums
いずれかの値を取る制約で、TSではユニオンリテラルになる
const values = z.enum(["foo", "bar", "baz"]); // foo, bar, bazのいずれか
type Enum = z.infer<typeof values>; // 'foo' | 'bar' | 'baz'
変数を介する場合は、型推論が効くように as const
をつける
const VALUES = ["Salmon", "Tuna", "Trout"] as const;
const FishEnum = z.enum(VALUES);
値を取り出すこともできる
const e = z.enum(["foo", "bar", "baz"]);
console.log(e.enum); // { foo: 'foo', bar: 'bar', baz: 'baz' }
console.log(e.options); // ['foo', 'bar', 'baz']
TypeScript の enum を使うこともまぁできるが推奨はされてない。
enum Fruits {
Apple,
Banana,
}
const FruitEnum = z.nativeEnum(Fruits);
type FruitEnum = z.infer<typeof FruitEnum>; // Fruits
FruitEnum.parse(Fruits.Apple); // passes
FruitEnum.parse(Fruits.Banana); // passes
FruitEnum.parse(0); // passes
FruitEnum.parse(1); // passes
FruitEnum.parse(3); // fails
Optionals
スキーマをオプショナル(undefined
とのユニオン) にする
const schema = z.object({
name: z.optional(z.string()), // ラップするパターン
age: z.number().optional(), // チェインするパターン
});
type Type = z.infer<typeof schema>; // { name?: string | undefined; age?: number | undefined; }
オプショナル化したスキーマをアンラップすることもできる。
const requiredSchema = z.object({
name: z.optional(z.string()).unwrap(), // ラップするパターン
age: z.number().optional().unwrap(), // チェインするパターン
});
type Type = z.infer<typeof requiredSchema>; // { name: string; age: number; }
Nullables
同様に nullable (Null とのユニオン)
z.nullable(z.string());
z.string().nullable();
unwrap は optional と同じ
z.string().nullable().unwrap();
Objects
これまでもちょいちょい出てきたけど、キーバリュー形式のデータ構造のスキーマを定義するのに使う。
const Dog = z.object({
name: z.string(),
age: z.number(),
});
便利メソッド
object
は特に多機能でいろんなメソッドが生えてるのでサクッと流す。
Dog.shape.name; // 各プロパティの型を参照できる
Dog.keyof(); // ['name', 'age']
Dog.extend({ bark: z.string() }); // スキーマを拡張する
Dog.merge(z.object({ bark: z.string() })); // スキーマをマージする
Dog.pick({ name: true }); // スキーマから一部を抜き出す
Dog.omit({ age: true }); // スキーマから一部を省く
Dog.partial() // 各プロパティをオプショナルにする
Dog.deepPartial() // 各プロパティを再帰的にオプショナルにする
Dog.required() // 各プロパティを必須にする
.passthrough
Zod オブジェクトはデフォルトではスキーマで定義されていないプロパティはバリデーション時に除外される。
const personSchema = z.object({
name: z.string(),
});
const person = personSchema.parse({
name: "lorem",
age: 43,
});
console.log(person); // { name: 'lorem' }
.passthrough()
を挟むことで、これを除外せずに無視してくれるようになる。
const person = personSchema.passthrough().parse({
name: "lorem",
age: 43,
});
console.log(person); // { name: 'lorem', age: 43 }
.strict
逆に .strict()
を挟むことで、余計なフィールドがあった場合に例外を投げるようになる。
const personSchema = z.object({
name: z.string(),
});
const person = personSchema.strict().parse({
name: "lorem",
age: 43,
});
console.log(person); // => throws ZodError
.strip()
passthrough
や strict
の制約をリセットする
const person = personSchema.passthrough().strict().strip().parse({
name: "lorem",
age: 43,
});
console.log(person); // => { name: 'lorem' }
.catchall
未定義のフィールドに対してまとめて実行するバリデーションを定義可能。
const personSchema = z.object({
name: z.string(),
}).catchall(z.number());
この場合、 name
以外のプロパティも number
なら許可される。
Array
言わずもがなの配列型。これもラップ形式とチェイン形式どちらもある。
z.array(z.string()) // string[]
z.string().array() // string[]
チェインの場合は順番に注意
z.string().optional().array() // (string | undefined)[]
z.string().array().optional() // (string[] | undefined)
便利メソッド
z.array(z.string()).nonempty() // 空配列は許可しない
z.array(z.string()).min(3) // 少なくとも3要素以上
z.array(z.string()).max(3) // 最大3要素まで
z.array(z.string()).length(3) // 3要素のみ
Tuples
タプルの定義
z.tuple([z.string(), z.number()]); // [string, number]
Unions
ユニオン型
z.union([z.boolean(), z.string()]); // boolean | string
z.boolean().or(z.string()); // boolean | string
Discriminated unions
ユニオン型と挙動はだいたい一緒だけど、多くのプロパティが共通で一部だけが異なるスキーマのユニオンの場合、どのフィールドを使って判定をするかを明示することができる。エラーメッセージがわかりやすくなるメリットがある。
const item = z
.discriminatedUnion("type", [
z.object({ type: z.literal("a"), a: z.string() }),
z.object({ type: z.literal("b"), b: z.string() }),
])
.parse({ type: "a", a: "abc" });
Records
TS の Record 型に対応
z.record(z.string(), z.number()); // Record<string, number>
キーに対して制約をかけることも可能
z.record(z.string().length(10), z.number()); // キーは10文字
Maps
マップ型
z.map(z.string(), z.number()); // Map<string, number>
Sets
セット型
z.set(z.string()); // Set<string>
Intersections
インターセクション型
const Person = z.object({
name: z.string()
})
const Employee = z.object({
role: z.string()
})
const EmployeePerson = z.intersection(Person, Employee); // { name: string } & { role: string }
あるいは and
も使用可
Person.and(Employee)
ただし大抵は .merge
を使用するほうが望ましい。 intersection
を使うと、 ZodIntersection
という新しい型のオブジェクトが返ってくるため、通常の ZodObject
にある pick
や omit
などが使えなくなり応用が効かなくなるため。
Recursive types
zod ではその仕組み上、自身を参照する再帰的なスキーマを定義する場合は冗長な定義が必要になる。
以下のように、自身の評価を後回しにする lazy を使用することと、型推論出来ないため明示的に interface を定義することが必要になる。
interface Category {
name: string;
subcategories: Category[];
}
const Category: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
subcategories: z.array(Category),
})
);
Promises
Promise 型のスキーマ
z.promise(z.number()); // Promise<number>
ただし parse
時の挙動はやや特殊
- ランタイムチェックでは Promise が何を返すのかは不明なので、Promise インスタンスであることをチェックする
-
parse
が async function となり、Promise が解決した時点で再度返り値を判定する
例えば以下では、 parse
を呼び出した時点では Promise インスタンスを渡しているのでバリデーションに成功するが、Promise が解決した3秒後にはエラーが確定する。
async function sleep(ms: number, value: any) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
const numberPromise = z.promise(z.number()); // Promise<number>
numberPromise.parse(sleep(3000, "test")).catch((e) => {
console.error(e); // Expected number, received string
});
Instanceof
あるクラスのインスタンスであることを検証するユーティリティ
class Test {
constructor(private name: string) {
this.name = name;
}
}
z.instanceof(Test).parse(new Test("test"));
Function schemas
関数についてもバリデーションが可能
z.function() // => () => unknown
z.function().args(z.string(), z.number()).returns(z.boolean()) // => (arg0: string, arg1: number) => boolean
なんとパラメータのバリデーションと実装を同時にできるし、実装がバリデータを満たしてない場合はランタイムエラーになる。
const f = z
.function()
.args(z.string(), z.number())
.returns(z.boolean())
.implement((str, num) => {
return true;
});
console.log(f("test", 1));
Preprocess
バリデーション前に入力データの変換を行う。 (通常は Zod はバリデーション後に変換を行うのが思想ではあるが)
const castToString = z.preprocess((val) => String(val), z.string());
console.log(castToString.parse(1)); // "1"
この場合は val
は unknown
になるため、どんな値でも渡せてしまうので注意
Schema methods
Zod スキーマに対して使用できるメソッドで、これまで登場したものはサクッと
// スキーマを元に値をバリデーションし、正常時はそのまま値を返す
schema.parse({ name: "John", age: 30 });
// バリデーション後に非同期のデータ変換がある場合は、parseAsyncを使う
schema.parseAsync({ name: "John", age: 30 });
// バリデーション失敗時も例外を投げず、エラー情報を返す
schema.safeParse({ name: "John", age: "30" });
// safeParse + parseAsync
schema.safeParseAsync({ name: "John", age: "30" });
// プロパティをオプショナル化したスキーマを返す
schema.optional();
// プロパティを Nullable にしたスキーマを返す
schema.nullable();
// プロパティを Nullish (nullable + optional) にしたスキーマを返す
schema.nullish();
// スキーマ全体を配列化したスキーマを返す
schema.array();
// スキーマ全体をPromise化したスキーマを返す
schema.promise();
// ユニオンスキーマを生成する
schema.or(z.string());
// インターセクションスキーマを生成する
schema.and(z.object({ foo: z.string() }));
.refine
カスタムバリデーションルールを定義できる。
z.string().refine((s) => s.length > 3, { message: "too short" });
refine は非同期関数を使用することもできるので、DBに問い合わせるといったランタイムバリデーションも可能。
const userId = z.string().refine(async (id) => {
// verify that ID exists in database
return true;
});
.superRefine
より高度なことができる refine
で、refine
も実態はこれのシンタックスシュガー。
ほんとに色々出来るみたいなのでサンプルコードだけ引用して省略
const Strings = z.array(z.string()).superRefine((val, ctx) => {
if (val.length > 3) {
ctx.addIssue({
code: z.ZodIssueCode.too_big,
maximum: 3,
type: "array",
inclusive: true,
message: "Too many items 😡",
});
}
if (val.length !== new Set(val).size) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `No duplicates allowed.`,
});
}
});
transform
バリデーション後、または途中でデータ変換を行う
z.string().transform((val) => val.length);
default
プロパティをオプショナルとし、省略された場合のデフォルト値を設定できる。
const schema = z.number().default(0);
console.log(schema.parse(undefined)); // 0
プリミティブのデフォルト値に関数を指定した場合は都度実行される
const schema = z.number().default(Math.random);
console.log(schema.parse(undefined)); // 毎回
console.log(schema.parse(undefined)); // 違う
console.log(schema.parse(undefined)); // 値になる
.brand
TypeScript は通常、構造的部分型を採用しているため、構造が合致していれば同じ型とみなすため Zod もそれに従うが、 .brand
を使用することで同じ Zod スキーマから生成した値でなければ違う型とみなすことができる。
// { name: string } の型に Cat という名前をつける
const Cat = z.object({ name: z.string() }).brand<"Cat">();
// Cat スキーマの型を用意しておく
type Cat = z.infer<typeof Cat>;
// Cat 型のみ受け取る関数
function sayHello(cat: Cat) {
console.log(cat.name);
}
// simba は Cat 型のため、sayHello に渡せる
const simba = Cat.parse({ name: "Simba" });
sayHello(simba);
// 以下は構造的部分型に従えば Cat 型を満たしてはいるが、渡すことができない
sayHello({ name: "Simba" });
Guides and concepts
簡単な活用手法についてのまとめ
Type inference
infer
を使えばスキーマから型を抜き出せる
const A = z.string();
type A = z.infer<typeof A>; // string
ただし、 transform
をした場合、バリデーションの中でデータの変換が行われるため、入力と出力で型が異なる場合がある。
// 入力は string だけど出力は number になる
z.string().transform((val) => val.length);
この場合は input
output
を使用して型推論する
type input = z.input<typeof stringToNumber>; // string
type output = z.output<typeof stringToNumber>; // number
Writing generic functions
任意の Zod スキーマを受け取る関数を書く場合
function makeSchemaOptional<T extends z.ZodTypeAny>(schema: T) {
return schema.optional();
}
string 型の Zod スキーマに絞る場合
function makeSchemaOptional<T extends z.ZodType<string>>(schema: T) {
return schema.optional();
}
Error handling
Zod はバリデーション失敗時に ZodError 型のエラーを返す。
詳細仕様はここに
Error formatting
エラーをシンプルなオブジェクトに変換できる
const data = z
.object({
name: z.string(),
})
.safeParse({ name: 12 });
if (!data.success) {
const formatted = data.error.format();
/* {
name: { _errors: [ 'Expected string, received number' ] }
} */
formatted.name?._errors;
// => ["Expected string, received number"]
}
Comparison
他のバリデーションライブラリとの比較
Joi
バリデーションライブラリだけど型推論機能は無い
Yup
zod と近いけど、zod には promise / function / union / intersection とより細かなスキーマを定義できる
io-ts
zod の API に影響を与えたライブラリ。
関数型プログラミングにフォーカスしており、fp-ts にも依存する上、 promise / function などが無い
Runtypes
readonly のような zod にない機能も有しているが、 pick / omit / extend などに欠ける
Ow
関数のパラメータの検証に特化しており、TypeScript の型システムを超えた型を表現できる。