はじめまして、Arganoの池田です。

本記事ではバリデーションライブラリZodについて紹介します。

Zod とは

ZodはTypeScript-firstなスキーマ定義とバリデーションを提供するライブラリです。Zodは他のバリデーションライブラリと比べてTypescriptとの相性が非常に良く、依存パッケージが無いことやバンドルサイズが小さいといった特徴があります。Zodの利用方法などについて紹介しながらTypescriptとの相性の良さについて見ていきます。

基本動作

TypescriptでZodを利用する際の一例を以下に示します。Zodのスキーマ定義は例として以下のように記述することができます。

import { z } from 'zod'

const user = z.object({
  name: z.string(),
  email: z.string().email(),
})

Zodではスキーマ定義からバリデーションのインターフェースをTypeScript型で生成することができます。スキーマ定義がより複雑化した際にこれらの型定義に悩まされなくて良いという点で非常に便利な機能でTypescriptとの相性の良さが表れています。

type UserInput = z.input<typeof user>
type UserOutput = z.output<typeof user>

バリデーションはuser.parseによって実行され上記の型情報にある通り入力はUserOutput型にパースされて帰ってきます。また、バリデーション失敗時にはエラーの詳細を含んだZodErrorオブジェクトがthrowされます。

try {
  const parsed = user.parse({
    name: "name",
    email: "email",
  })
} catch (e) {
  if (instanceof ZodError) {
    console.log(e)
    // ZodError: [
    //   {
    //     "validation": "email",
    //     "code": "invalid_string",
    //     "message": "Invalid email",
    //     "path": [
    //       "email"
    //     ]
    //   }
    // ]
  }
}

利用例

基本的な使い方は以上です。次はより複雑なスキーマを定義してみます。以下のようなユーザー情報がフォームに入力され、これを元に入力を検証しながら出力を適当な型に変換するような場合を想定します。

fieldschema
namestring(必須)
emailemail(必須)
address.activeboolean
address.zipCodestring(address.activeがtrueならば必須、falseならば入力不可)
address.citystring(address.activeがtrueならば必須、falseならば入力不可)

スキーマ定義は以下のようになります。

const user = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  address: z
    .discriminatedUnion('active', [
      z.object({
        active: z.literal(true),
        zipCode: z.string().min(1),
        city: z.string().min(1),
      }),
      z.object({
        active: z.literal(false),
        zipCode: z.undefined(),
        city: z.undefined(),
      }),
    ])
    .transform((address) => {
      if (!address.active) return undefined
      return (({ zipCode, city }) => ({ zipCode, city }))(address)
    }),
})

discriminatedUnionは第一引数に指定されたキーにより入力に適用するスキーマをUnionの中から識別することができるので、これにより本来は必要な条件分岐を必要とせずに適切なバリデーションを適用できます。

type UserInput = z.input<typeof user>
// type UserInput = {
//   name: string
//   email: string
//   address: {
//     active: true
//     zipCode: string
//     city: string
//   } | {
//     active: false
//     zipCode?: undefined
//     city?: undefined
//   }
// }
type UserOutput = z.output<typeof user>
// type UserOutput = {
//   name: string
//   email: string
//   address?: {
//     zipCode: string
//     city: string
//   } | undefined
// }

結果を確認してみます。safeParseは例外を投げることなくバリデーション結果を確認できます。

let result = user.safeParse({
  name: 'name',
  email: 'test@email.com',
  address: {
    active: false,
  },
})
console.log(result)
// {
//  "success": true,
//  "data": {
//    "name": "name",
//    "email": "test@email.com"
//  }
// }
result = user.safeParse({
  name: 'name',
  email: 'test@email.com',
  address: {
    active: true,
  }
})
console.log(result)
// {
//   "success": false,
//   "error": {
//     "issues": [
//       {
//           "code": "invalid_type",
//           "expected": "string",
//           "received": "undefined",
//           "path": [
//               "address",
//               "zipCode"
//           ],
//           "message": "Required"
//       },
//       {
//           "code": "invalid_type",
//           "expected": "string",
//           "received": "undefined",
//           "path": [
//               "address",
//               "city"
//           ],
//           "message": "Required"
//       }
//     ]
//   }
// }
result = user.safeParse({
  name: 'name',
  email: 'test@email.com',
  address: {
    active: true,
    zipCode: '000-0001',
    city: 'city',
  },
})
console.log(result)
// {
//   "success": true,
//   "data": {
//     "name": "name",
//     "email": "test@email.com",
//     "address": {
//       "zipCode": "000-0001",
//       "city": "city"
//     }
//   }
// }

バリデーションの実行がvalidateではなくparseと表現されていることからも分かる通りスキーマ定義次第で出力の型を柔軟に変換することができます。これによってaddress.active = falseのときは結果からuser.addressが除外されていますがバリデーション自体は期待通り成功しています。また、address.active = trueのときはuser.address.zipCodeuser.address.cityが必須項目のためエラーとなっていることが分かります。

以上のようにTypescriptの型定義の力を借りながら柔軟なバリデーションの実装ができるというZodの特徴を確認することができました。