Zodのドキュメントを読みながら自分用にメモ

基本的なことは大丈夫なのでざっと読んで気になったところだけをメモしておく。使ってるときに「そういえばこんな機能が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.inferz.output と同じ。

ふだんから z.infer じゃなくて z.inputz.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のこと思い出したくなったらこの記事を自分で読めば思い出せそう。楽しかった。