データ形式の検証ライブラリ、Zodを使う 第1回 Zodの特徴と基本的な使い方

Astro v2の新機能Content Collectionsでも採用されたデータ検証ライブラリZod。TypeScriptとの役割の違いに焦点を当て、基本的な使い方を解説します。

発行

著者 菅野 亜実 フロントエンド・エンジニア
データ形式の検証ライブラリ、Zodを使う シリーズの記事一覧

データ検証ライブラリ、Zod

Zodは、JavaScriptで独自のデータ型(スキーマ)を定義し、値がその型に合致しているか検証するためのライブラリです。

合致しない場合は独自のエラーメッセージを返したり、入力値を適切なフォーマットに変換してから取り出せたりと、入力フォームやNode.jsで構築されたバックエンドAPIの安全性や利便性を向上させる上で役立ちます。

ZodとTypeScriptの違い:実行時検証と静的検証

TypeScriptとZodは、どちらも型チェックの機能を持ちますが、型チェックを開発時に行うか、実行時に行うか、という違いから、その役割も大きく異なります。

TypeScriptは、型チェックの機能を持つ言語であり、エディタに対して入力補完やエラー検出を強化する機能を提供します。 TypeScriptのプログラムはそのままではブラウザでは実行できず、必ずJavaScriptへのコンパイルを行います。

このJavaScriptへのコンパイル時に、既にプログラム中で用意されているデータ(変数など)の型を認識し、そのデータが持たないメソッドの呼び出しや、そのデータに適用できない演算、その型のデータを受け付けない関数の呼び出しなどに対してコンパイルエラーを出すことで、実行時にエラーになる可能性のあるコードを修正するよう促します。

このように、コードを実行せずに行う型チェックを静的解析といいます。

しかし、TypeScriptは「言語」であり、TypeScriptで書いたからといって、「型検証処理」を書いたことにはなりません。

たとえば、次のように関数の引数の型を指定したとします。

2つの引数xとyはどちらもnumber型であると定義

const add = (x: number, y: number) => x + y

引数の値が最初からわかっているのであれば、誤った型の引数が渡された場合、型としてはエラーとなり、JavaScriptへのコンパイルは成功しないでしょう。

引数として文字列を渡してしまう(型エラーが発生する)

add("4px", "2em")

しかし、Webアプリケーションでは、実際にどんなデータが渡されるのかわからない場合も多くあります。次のような、実行中に動的に作成されるデータを扱う機会が多いからです。

  • 入力フォームなどのユーザ操作によって作られるデータ
  • TS/JSの変数ではなく、別ファイル(JSON、YAML、Markdownなど)からインポートされるデータ
  • APIから取得するデータ

これらのデータを格納する変数に型をつけたとしても、取得したデータが本当にその型に沿っていることが保証されているわけではありません。

fetchで取得した外部データを引数として渡す(型エラーは発生しない)

interface Data {
  value1: number;
  value2: number;
}

// サーバーからデータを取得する
const serverData = await fetch("https://example.com")
// 取得したデータは、Data型通りの内容だと想定
const data: Data = await serverData.json()

// この引数は本当にnumber型でしょうか?
// example.comが返すデータは、こちらの想定通りとは限らない
// 実際に取得したdataのvalue1やvalue2が数値でなかったとしても、add関数は実行されてしまう
add(data.value1, data.value2)

TypeScriptで引数の型を制限していたとしても、コンパイルによって生成されたJavaScriptプログラムには型の情報は残っていないため、実行時に誤った形式のデータを弾く処理が行われるわけではないのです。

型定義だけでなく、実際に実行されるJavaScriptプログラムとして、誤った形式のデータ(この場合は引数)に対してエラーを返したり、適切なフォーマットに変換したりといった処理を記述する必要があります。

たとえば、値が数値かどうかを実行時に判定するには、typeof value1 === 'number'という条件式を使えばよいでしょう。

しかし、異なる型のデータが登場するたびにどんどんコードが複雑になってしまいます。 「10以下の整数」「@から始まる文字列」などといった細かい制約を追加するのも一苦労です。

Zodを使うと、データの仕様や変換の流れを宣言するような記法で、このような実行時の検証、エラーハンドリング、フォーマット変換などの処理を簡潔に記述することができます。

Zodを使う準備

Zodを使うメリットを一通り解説しましたので、ここからは実際にZodを使ってみましょう。

インストール

Zodは、npmやyarnですぐに導入することができます。

npmの場合

npm install zod

yarnの場合

yarn add zod

TypeScriptと併用する場合の設定

Zodは、strictモードを有効にしたTypeScript環境で使うことが推奨されています。

プロジェクトのルートディレクトリにtsconfig.jsonを置いてください。Zodの動作に必要な最小限の記述は、次のとおりです。

tsconfig.json

{
  "compilerOptions": {
    "strict": true
  }
}

Zodスキーマオブジェクト

今回は、インポートしたJSONファイルが所定のフォーマットに適合しているか、検証するスクリプトをZodで書いてみます。本記事の解説に使用されているサンプルプログラムは、次のリポジトリの01ディレクトリ内に掲載しています。

データ形式の検証ライブラリ、Zodを使う

まずは、01ディレクトリ内でnpm iyarnを実行することで、依存ライブラリをインストールしてください。zodライブラリからインポートしたzオブジェクトによって、Zodの全機能にアクセスすることができます。

zオブジェクト

import { z } from "zod"

そして、z.型名()という形の構文によって、その型であるかどうかを検証するためのAPIを備えたオブジェクトを得ることができます。

srcとaltはstring型、widthとheightはnumber型とする

import { z } from "zod"

// srcプロパティの検証に使うオブジェクト
const zSrc = z.string()

// altプロパティの検証に使うオブジェクト
const zAlt = z.string()

// widthプロパティの検証に使うオブジェクト
const zWidth = z.number()

// heightプロパティの検証に使うオブジェクト
const zHeight = z.number()

検証ルールを追加する

z.型名()によって得られるオブジェクトには、その型に応じた検証ルールを追加するメソッドが備わっています。これらのメソッドをチェーン方式で繋げて書くことで、細かい検証ルールを表現することができます。

文字列のフォーマットに関する検証ルール

たとえば、文字列型で空文字を許容したくない場合は次のように書けます。

altは必ず記入するよう検証ルールを追加

// trim()で空白を削除した上で、1文字以上かどうかをチェック
const zAlt = z.string().trim().min(1)

上のコードでは、まずstringメソッドで渡されたデータが文字列であることを確認します。stringメソッドによる検証に合格し、データが文字列だとわかったら、trimメソッドで前後の空白を削除します。その上で、min(1)メソッドによって1文字以上かどうかをチェックすることで、文字を必ず入力するよう要求する検証ルールが完成します。

また、URLやメールアドレスなど、よく使われるフォーマットの文字列検証メソッドも用意されています。

Zodが提供する文字列検証メソッドを使えば、自前で複雑な正規表現を書くことなく、手軽に検証することができます。

srcにはURLを記入するよう検証ルールを追加

const zSrc = z.string().trim().url()

上のコードは、文字列であることを確認し、空白を削除してから、URL文字列を表現した正規表現に合致するかどうかをurlメソッドでチェックします。

Zodの提供するurlメソッドは、http://https://だけでなく、file://などのスキームも許容します。

http、もしくはhttpsで始まるWeb上のURLのみを許容したい場合は、Zodが提供するstartsWithメソッドを併用しましょう。

srcにはWeb上のURLを記入するよう検証ルールを追加

const zSrc = z.string().trim().url().startsWith('http')

同様に、〜で終わる文字列を表現するendsWithメソッドもあります。

srcにはpngを拡張子に持つ画像URLを指定するよう検証ルールを追加

const zSrc = z.string().trim().url().startsWith('http').endsWith('.png')

数値に関する検証ルール

数値型のための検証メソッドも多数提供されています。

100以上500以下の数値に限定する

const zSize = z.number().min(100).max(500)

ほかにも、正負を検証するメソッドや、「未満」を表すlt(less than)メソッド、「〜より大きい」を表すgt(greater than)メソッド、「〜の倍数」を表すmultipleOfメソッドなどがあります。

オブジェクトと配列

ここまでで定義した各プロパティのスキーマをまとめて、実際にJSONファイルに格納されているデータのスキーマを完成させましょう。

z.objectメソッドの引数に、プロパティ名とその検証オブジェクトを対応させたオブジェクトを指定することで、オブジェクト型のZodスキーマを定義できます。

画像情報オブジェクトを表すZodスキーマ

const zImage = z.object({
  src: zSrc,
  alt: zAlt,
  width: zSize,
  height: zSize
})

上のコードでは、まず渡されたデータがsrc、alt、width、heightの4つのプロパティを持つオブジェクトかどうかを確認し、その後、それぞれのプロパティに指定したスキーマ(zSrc, zAltzSize)に基づいて、各プロパティの値をチェックするという検証ルールを定義しています。

また、続けてarrayメソッドを呼び出すことで、その型の配列を表すZodスキーマを作ることができます。

画像情報オブジェクトの配列を表すZodスキーマ

const zImages = zImage.array()

上のように書くことで、渡されたデータが配列であり、その要素がすべてzImageスキーマに合致するオブジェクトであることを確認する検証ルールが定義されます。

入力値の検証

zImagesスキーマが想定するデータは、次のようなものです。

zImageスキーマに適合するJSON形式のデータ

[
  {
    "src": "https://polygan.edu/rcaffey.png",
    "alt": "Rolf Caffey",
    "width": 200,
    "height": 320
  },
  {
    "src": "https://keysoft.eu/jwebster.png",
    "alt": "Joslyn Webster",
    "width": 260,
    "height": 200
  }
]

実際にデータを検証するには、Zodスキーマオブジェクトが持つsafeParseメソッドを呼び出します。

先ほどのJSON形式のデータをzImagesスキーマで検証

// データ(json)がzImagesスキーマに適合するかどうか、検証して結果を取得
const result = zImages.safeParse(json)

今回は、zImagesスキーマに適合するデータが渡されているため、検証に合格し、resultの中身は次のようになります。

検証結果(検証に合格した)

{
  success: true,
  data: [
    {
      src: 'https://polygan.edu/rcaffey.png',
      alt: 'Rolf Caffey',
      width: 200,
      height: 320
    },
    {
      src: 'https://keysoft.eu/jwebster.png',
      alt: 'Joslyn Webster',
      width: 260,
      height: 200
    }
  ]
}

safeParseメソッドは、検証ルールに合格した場合、success: trueフラグとともにデータを返します。しかし、このとき返されるデータ(dataプロパティの値)は、入力したデータそのままとは限りません。

たとえば、zAltzSrcの検証ルールの中では、trimメソッドを呼び出して空白を削除しています。

このように、検証の最中に値を変換した場合には、変換された値が結果オブジェクトのdataとして返ってくるのです。

前後に空白がついた文字列をaltスキーマに渡して検証

const result = zAlt.safeParse("    Rolf Caffey    ")

この場合のresultは次のような内容になり、確かに前後の余白が削除されています。

検証結果

{ success: true, data: 'Rolf Caffey' }

この特性から、Zodはデータの検証だけでなく、データを取り出す前に適切なフォーマットに変換する前処理を合わせて行うことができます。

trimは前後の空白を除去するためだけのユーティリティですが、transformメソッドを使うと、独自の変換処理を組み込むことができます。

transformメソッドについては、第3回で詳しくご紹介します。

エラーハンドリング

データの検証でもっとも重要なのはエラーの制御であり、わかりやすいエラーメッセージが得られて、簡単にエラー箇所を特定できることが望まれます。

Zodではユースケースに応じた、さまざまなデータ形式でエラーを抽出する手段が用意されています。

実際に、定義したスキーマに適合しないオブジェクトをZodで検証し、エラーを覗いてみましょう。

zImagesスキーマに適合しないデータ

[
  {
    // Bad: 完全なURLではない
    "src": "nitrocam.club/rmuldoon.png",
    // Bad: 未入力
    "alt": "",
    // Bad: 100未満
    "width": 10,
    // Bad: 500以上
    "height": 1000
  },
  {
    // Bad: httpで始まるURLではない、拡張子がpngではない
    "src": "file://chromaton/ehancock.svg",
    // Bad: 数値ではない
    "width": "100px",
    "height": 350
    // Bad: altが抜けている
  }
]

このようなデータは、zImagesスキーマによる検証では不合格となります。

safeParseメソッドは、検証に合格しなかった場合、success: falseフラグとともにエラー情報オブジェクトを返します。

不合格時の検証結果オブジェクトの構造

{
  success: false,
  error: {/** エラー情報 */}
}

このerrorプロパティでアクセスできるオブジェクトは、いくつかのプロパティとメソッドを備えており、それらを経由して必要な情報を抽出することができます。

  • エラー発生箇所ごとにエラーメッセージをまとめるformatメソッド
  • エラーメッセージを一覧化するflattenメソッド
  • 詳細なエラー情報を得るissuesプロパティ

一つずつ見ていきましょう。

formatメソッド

formatメソッドは、エラー発生箇所ごとにエラーメッセージをまとめてくれるものです。

formatメソッドでエラー情報を取得する

if (!result.success) {
  const formattedError = result.error.format()
}

formatメソッドで得られるオブジェクトは、プロパティがエラー発生箇所へのパスとなっており、最終的に辿り着く_errorsプロパティがその箇所に関するエラーメッセージになっています。

たとえば、formattedError[0].src?._errorsにアクセスすれば、JSON配列内の最初の要素のsrcプロパティに関するエラーメッセージが配列で得られるわけです。

formatメソッドで得られるオブジェクト

{
  "0": {
    "_errors": [],
    "src": {
      "_errors": [
        "Invalid url",
        "Invalid input: must start with \"http\""
      ]
    },
    "alt": {
      "_errors": [
        "String must contain at least 1 character(s)"
      ]
    },
    "width": {
      "_errors": [
        "Number must be greater than or equal to 100"
      ]
    },
    "height": {
      "_errors": [
        "Number must be less than or equal to 500"
      ]
    }
  },
  "1": {
    "_errors": [],
    "src": {
      "_errors": [
        "Invalid input: must start with \"http\"",
        "Invalid input: must end with \".png\""
      ]
    },
    "alt": {
      "_errors": [
        "Required"
      ]
    },
    "width": {
      "_errors": [
        "Expected number, received string"
      ]
    }
  },
  "_errors": []
}

formatメソッドは、たとえば入力フォームの各入力欄の上に、その入力項目に関するエラーメッセージを表示したい場合など、エラーを個別に処理したい場合に役立ちます。

flattenメソッド

一方、入力フォームの送信時に、送信ボタンの上にすべての項目のエラーメッセージをまとめて表示したい場合などは、formatメソッドでは走査が面倒です。

flattenメソッドを使うと、ひとつのデータオブジェクトに関する全エラーメッセージを一気に取得することができます。

flattenメソッドでエラー情報を取得する

if (!result.success) {
  const flatError = result.error.flatten()
}

配列内の各オブジェクトごとにエラーメッセージが配列にまとめられる一方で、オブジェクト内のどのプロパティに関するエラーなのかという、詳細な位置情報は消えてしまいます。

flattenメソッドで得られるオブジェクト

{
  "formErrors": [],
  "fieldErrors": {
    "0": [
      "Invalid url",
      "Invalid input: must start with \"http\"",
      "String must contain at least 1 character(s)",
      "Number must be greater than or equal to 100",
      "Number must be less than or equal to 500"
    ],
    "1": [
      "Invalid input: must start with \"http\"",
      "Invalid input: must end with \".png\"",
      "Required",
      "Expected number, received string"
    ]
  }
}

issuesプロパティ

メッセージだけではなく、完全なエラー情報を得たい場合は、issuesプロパティにアクセスします。

issuesプロパティで完全なエラー情報を取得する

if (!result.success) {
  const issues = result.error.issues
}

issuesは、エラーの詳細情報を格納したオブジェクト(ZodIssueインスタンス)の配列です。

例:widthに"100px"を指定した部分に対するエラー情報

  {
    // エラーコード
    "code": "invalid_type",
    // 期待される型
    "expected": "number",
    // 実際に入力されたデータの型
    "received": "string",
    // エラー発生箇所
    "path": [
      1,
      "width"
    ],
    // エラーメッセージ
    "message": "Expected number, received string"
  }

ZodIssueインスタンスは、codepathmessageプロパティを必ず持ちます。

pathを変数に保存したり、表示したりしたい場合は、formatメソッドで得られるオブジェクトのキーを辿って集めるよりも、issuesを直接走査したほうが簡潔かもしれません。

完全なissuesの中身と、issuesを使ったエラー解析例は、デモリポジトリに掲載しています。

safeParseメソッドとparseメソッドの挙動の違い

Zodには、safeParseparseという2種類の検証メソッドが用意されています。

safeParseメソッドの返り値は常にオブジェクトで、successフラグによって検証結果を知り、そこから処理を分岐させることができます。

safeParseメソッドで検証し、処理を分岐させる例

// 検証結果を取得
const result = zHogeSchema.safeParse(data)

// sucessフラグによって分岐
if (result.success) {
  // 合格したデータ
  const validData = result.data
} else {
  // エラー情報オブジェクト(ZodErrorインスタンス)
  const errorInfo = result.error
}

一方、parseメソッドは、検証ルールに沿わない値が与えられた場合にエラーがスローされます。

parseメソッドで検証し、処理を分岐させる例

try {
  // 合格したデータ
  const validData = zHogeSchema.parse(data)
} catch (err) {
  if (err instanceof z.ZodError) {
    // エラー情報オブジェクト(ZodErrorインスタンス)
    const errorInfo = err
  }
}

これらの検証メソッドは使い方が異なるだけで、得られる結果に差はありません。

parseメソッドが検証不合格時にthrowするエラーオブジェクトも、safeParseメソッドで得られるエラーオブジェクトと同様のメソッドやプロパティを持ち、ここまで解説してきた手法でエラーを処理することができます。

まとめ

今回は、Zodの立ち位置を明確にした上で、基本的な検証ルールの表現と検証実行の流れを解説しました。

検証結果からエラーを取り出す方法についても触れましたが、得られたエラーメッセージはすべて英語です。

次回は、エラーメッセージを要件に合わせてカスタマイズしながら、値同士の比較検証や、複数パターンを許可するOR条件など、複数のスキーマが関わる検証について考えていきます。