基本的なことは大丈夫なのでざっと読んで気になったところだけをメモしておく。使ってるときに「そういえばこんな機能がZodにあった気がする」って頭の中で引っかかるように。
視点としては、Webアプリケーションのサーバーサイドを作る頭で読んでる。フレームワークを作る頭とかではない。
Coercion
コンストラクタ関数を噛ませて変換するやつ。
z.coerce.string()
Literals
忘れたりはしないだろうけどメモ
z.literal("tuna")
Strings
datetime
ZodStringにくっついてくる。タイムゾーン有無・精度・ローカルとかある。
const schema = z.string().datetime({ local: true }); schema.parse("2020-01-01T00:00:00"); // pass
datetime以外に、dateもtimeもある。日付文字列を扱うやつだから、日付型を扱うやつと頭の中で混乱しないように注意しておきたい。
Zod enums
へー。ZodEnumには.enum
なんてのがあるんだ。値だけを返す .options
もある。
.extract
と .exclude
もある。
(使わない) Native enums
僕は使わない
.optional
.nullable
.nullish
ラッピングする関数もあるけど
const schema = z.optional(z.string());
↓僕はメソッドチェーンの方を使いそう
const schema = z.string().optional();
.nullable()
もそう。
nullとundefinedを両方許容する .nullish()
もある。
.unwrap()
で中のスキーマを取り出せる。
Objects
(たぶん使わない) .shape
プロパティのスキーマにアクセスできる
Dog.shape.name;
(たぶん使わない) .keyof
キーのZodEnumを生成する。へー。ZodEnumなのか。
const keySchema = Dog.keyof(); keySchema; // ZodEnum<["name", "age"]>
.extend
フィールドを追加できる。上書きもできるので注意
const DogWithBreed = Dog.extend({ breed: z.string(), });
.merge
ZodObject同士をマージする。A.extend(B.shape)
と同じ。へー .shape
か。そっか。
BaseTeacher.merge(HasID);
同じフィールドは上書きされる。 知らないキーに対するポリシーやキャッチオールスキーマも後ろのスキーマのもので上書きされる(このあたりは、あとで出てくる)。
(たぶん使わない) .pick
.omit
.partial
.deepPartial
.required
このあたりはスキーマの再定義を避けたいときに使うかなぁって思うけど、あんまりそういう機会はないかなぁと想像。
.pick
一部のプロパティを取り出したZodObjectを生成.omit
一部のプロパティを除外したZodObjectを生成.partial
全プロパティをオプショナルにしたZodObjectを生成。ただし1階層のみ。オプショナルにするプロパティを指定することも可能。.deepPartial
全階層をオプショナルにする.required
全プロパティを必須にしたZodObjectを生成。.partial
の逆。
.paththrough
デフォルトではZodはparseするときに知らないキーは除去する。
const person = z.object({ name: z.string(), }); person.parse({ name: "bob dylan", extraKey: 61, }); // => { name: "bob dylan" } // extraKey has been stripped
.paththrough
を使うと、除去しない。
person.passthrough().parse({ name: "bob dylan", extraKey: 61, }); // => { name: "bob dylan", extraKey: 61 }
あんまり知らないキーは入ってきてほしくないから、デフォルトの動きで使いたいところだな。
.strict
知らないキーを無視するんじゃなくて、エラーにする。
const person = z .object({ name: z.string(), }) .strict(); person.parse({ name: "bob dylan", extraKey: 61, }); // => throws ZodError
.strip
デフォルトの動き(知らないキーを除去する)に戻す。
(たぶん使わない) .catchall
知らないキーに対する共通のバリデーションを定義する。
const person = z .object({ name: z.string(), }) .catchall(z.number()); person.parse({ name: "bob dylan", validExtraKey: 61, // works fine }); person.parse({ name: "bob dylan", validExtraKey: false, // fails }); // => throws ZodError
.catchall
を使うと、すべてのキーが「既知のキー」扱いになるから .paththrough()
.strip()
.strict()
は意味がなくなる。
Arrays
const stringArray = z.array(z.string()); // equivalent const stringArray = z.string().array();
どっち使うか悩むところだけど、これは前者のほうが僕は好きだな。
.element
要素のスキーマにアクセスできる。
.nonempty
「要素が1つは必要」という定義ができる。.nonempty
を使うと infer
したときの型が [string, ...string[]]
みたいな可変長タプルになって、1つ目が型として必須になる。へー。
.min
.max
.length
要素数に制約を加えられる。こっちは .nonempty
と違って型に変化はない。
(たぶん使わない) Tuples
タプルも定義できる。僕は、使う機会はそんなになさそう。
const schema = z.tuple([z.string(), z.number()]);
可変長タプルの場合は .rest
で定義できる
const variadicTuple = z.tuple([z.string()]).rest(z.number()); // => [string, ...number[]];
Unions
複数のスキーマを定義できて、定義した順番にバリデーションを実行して、最初に成功した値を返す。
const stringOrNumber = z.union([z.string(), z.number()]); const stringOrNumber = z.string().or(z.number());
んー。書き方は前者の方が好きかな。ただ、これバリデーションを複数回実行するから効率はよくないよな。
↓こんな風に書くと「URLの形式の文字列か空文字列」でチェックできる。この書き方は覚えておいてもいいかも。
const optionalUrl = z.union([z.string().url(), z.literal("")]);
あぁ、でもこの用途のときは or
で書くほうがいいかなぁ。
const optionalUrl = z.string().url().or(z.literal(""));
Discriminated unions
.union
だと全部の候補をチェックするけど .discriminatedUnion
を使うと判別のキーを指定するので1つだけのチェックで済む。
const myUnion = z.discriminatedUnion("status", [ z.object({ status: z.literal("success"), data: z.string() }), z.object({ status: z.literal("failed"), error: z.instanceof(Error) }), ]); myUnion.parse({ status: "success", data: "yippie ki yay" });
(たぶん使わない) Records, Maps, Sets, Intersections, Recursive types, Promises, Instanceof, Functions
このあたりがZodでバリデーションをかけたい部分に存在するかと言われると、あんまりないかなぁという気がする。必要になったらチェックする。
Intersectionsは、.merge
でも同じことができて、そっちのが便利っぽいから必要なときは .merge
を使うでよさそう。
(たぶん使わない) Preprocess
値のチェックをする前に変換できる。今だとCoercingが実装されてるから使う機会はなさそう。
Schema methods
.parse
.parseAsync
.safeParse
.safeParseAsync
このあたりは普通に使うからメモはいらないや。バリデーションでAsyncが必要な処理はあんまりやりたくないかな。
.refine
独自のバリデーションを定義できる
const myString = z.string().refine((val) => val.length <= 255, { message: "String can't be more than 255 characters", });
バリデーション失敗時は例外を投げるんじゃなくてfalsyな値を返す。第二引数に渡すオプションは失敗時にZodErrorで使われる。
(たぶん使わない) .superRefine
バリデーションを詳細に実装できる。.refine
が内部で使ってる。ここまで必要になることはあまりなさそう。必要になったら見るくらいでいいかな。
.transform
パース後のデータを変換する。
変換と同時にバリデーションを実行することもできる。その場合 .superRefine
と同じやり方になる。 ctx.addIssue()
を呼び出すと失敗とみなされる。たしかに変換するときにバリデーションかけてしまいたいことはちょくちょくありそう。
Async定義も可。その場合は .parseAsync
を使わないといけない。
.default
undefinedの場合に返す値を定義。値を返す関数を指定することも可能。
const stringWithDefault = z.string().default("tuna"); stringWithDefault.parse(undefined); // => "tuna" stringWithDefault.parse("a"); // => "a" stringWithDefault.parse("") // => "" stringWithDefault.parse(null) // => Error
undefinedのときにはバリデーションを実行せずにこの値が返されることを覚えておきたい。
.describe
説明文を指定できる。description
プロパティに設定される。ドキュメンテーションツールとかで使われてそう。
.catch
パースエラーのときに返される値を指定できる。関数を指定すると、失敗したコンテキストを受け取って処理ができる。
const numberWithCatch = z.number().catch(42);
.brand
Zodでパースしたものを型として区別できるようにしてくれる。
たとえば「1-10までの整数」をスキーマで定義して、それをもとにinferした型を受け取る関数を書いたとしても、TypeScript的にはnumber型にしかならない。.brand
をつけておくと「Zodのパースに成功した型」を型として表現できるようになるので、安心だねぇという話。
ただ、Branded Typesまわりは、すぐ頭から煙がでるので、もういちどちゃんと勉強しておきたい。このドキュメント読み終わったら調べよっと。
↓この記事とか面白いよねぇ。
.readonly
型がReadonlyになる。パースした値も Object.freeze()
で変更できなくしてくれる。
.pipe
transformしたあとにバリデーションをかけたい場合に使える。
z.string() .transform((val) => val.length) .pipe(z.number().min(5));
チェックをかけたあとに .coerce
したい場合にも使える
z.coerce.date().parse(null); // => new Date(null) で成功する z.union([z.number(), z.string(), z.date()]) .pipe(z.coerce.date()) .parse(null); // => nullはエラーになる
z.infer
z.input
z.output
スキーマから型を取得するには z.infer
が使える
const A = z.string(); type A = z.infer<typeof A>; // string
ただ .coerce
や .transform
や .brand
などを使う場合は、入力の型と出力の型が異なる。だから z.input
z.output
がある。
const stringToNumber = z.string().transform((val) => val.length); type input = z.input<typeof stringToNumber>; // string type output = z.output<typeof stringToNumber>; // number
z.infer
は z.output
と同じ。
ふだんから z.infer
じゃなくて z.input
と z.output
を使っておこうかな。
ZodType
Zodの型を使った汎用的な関数を書くときは「Writing generic functions」のところを読もうかな。このあたりはよく分かってない。
「z.discriminatedUnionを非推奨にしたい」の話はなくなってた
z.discriminatedUnion
は実装がいまいちでエッジケースが多いから、z.switch
を新しく作って z.discriminatedUnion
は非推奨にしたいなーって話はなくなったみたい。対応なしのままクローズされてた。
からの z.switch
つくるより z.union
の実装を改良してスピードをあげる案を思いついた!ってRFCが出てた。良さそう。Subscribeしとこっと。
ひととおり流し読みした
ひととおり流し読みして満足した。次はbrandedのところをもういちど勉強するか、zod-to-openapi のドキュメントを読むか、そのあたりかな。
これでZodのこと思い出したくなったらこの記事を自分で読めば思い出せそう。楽しかった。