Masayan tech blog.

  1. ブログ記事一覧>
  2. モダンフロントエンド開発に必須級のライブラリZodを紹介する

モダンフロントエンド開発に必須級のライブラリZodを紹介する

公開日

環境

  • macOS Monterey 12.6
  • Windows 11
  • VSCode
  • node.js 18.6.0
  • npm 8.13.2
  • Typescript 4.9
  • Zod 3.21.4

Zodとは?

Zodは、TypeScript Firstなバリデーションライブラリです。

zodを使用することで、データ型に加えてそのデータの文字列長や形式まで細かくチェックすることが可能になります。

例えば、zodを使用すると郵便番号の形式(ハイフンあり)について以下のように簡潔にバリデーションを実装することが可能です。

import { z } from 'zod';
const regExp = /^[0-9]{3}-[0-9]{4}$/g;
const ZipCodeSchema = z
  .string()
  .regex(regExp, { message: 'Invalid format zip code' });


const r1 = ZipCodeSchema.safeParse('500-0001');
console.log(r1); // {success: true, data: "500-0001"}

const r2 = ZipCodeSchema.safeParse('5000001');
console.log(r2); // {success: false, error: {…}}

また、以下のような特徴もあります

  • ライブラリ自体のサイズが小さいので、プロジェクトの導入しやすい
  • 依存関係がない(JsでもTsでも可能)
  • スキーマからTypeScriptの型がシームレスに生成できる(←これ重要)

基本的な使い方

スキーマの作成

zodでは、まずスキーマを作成します。スキーマには、型とバリデーションルール、エラーメッセージ(必要に応じて)などを設定します。

import { z } from 'zod';


const CustomerSchema = z.object({
  name: z.string(),
  age: z.number(),
  birthday: z.date(),
});

バリデーション実行

スキーマが作成出来たら、データをパースすることによりバリデーションOKかNGかを判定します。(スキーマに即したデータとなっているかそうでないか)

バリデーション実行時に使用できるパース用のメソッドには、parsesafeParseがあります。バリデーション成功時/失敗時の動作の違いは以下の通りです。

成功時

失敗時

parse

期待した型の値を返します

検証エラーを含むZodError(Errorクラスを継承)オブジェクトがthrowされます

safeParse

{success: true, data:{...}}のようなオブジェクトが返却されます

{success: false, error:{...}}のようなオブジェクトが返却されます

余談ですが、検証値がプリミティブ型ではない場合、検証結果は与えられた値とは異なる値になります。そのため、以下の厳密等価は常に偽となります

import { z } from 'zod';


const CustomerSchema = z.object({
  id: z.number(),
  name: z.string().max(25),
  email: z.string().email(),
});


const customer = { id: 1, name'会員A'email'test@test.com' };
const result = CustomerSchema.parse(customer);


console.log(result === customer); //false

多種多様なスキーマ定義

単純なstring、number、dateは上記のサンプルの通りですが、それ以外にも様々なスキーマの定義が可能です。

真偽値

import { z } from 'zod';


const CustomerSchema = z.object({
  name: z.string(),
  isLogin: z.boolean(),
});
const customer = { name'大阪太郎', isLogin: true };


/**
 * {name: "大阪太郎", isLogin: true}
 */
console.log(CustomerSchema.parse(customer));

配列

arrayを使用します。また、arrayの引数に配列の要素の型を指定します。

import { z } from 'zod';


const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
  followerIds: z.array(z.number()),
});
const customer = {
  name'大阪太郎',
  age: undefined,
  followerIds: [1479],
};


/**
 * {name"大阪太郎", age: undefined, followerIds: Array[4]}
 */
console.log(CustomerSchema.parse(customer));

HoegeSchema.shape.ArrayProperty.elementを形式で、配列要素のスキーマのみを取得することも可能です。

import { z } from 'zod';


const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
  followerIds: z.array(z.number()),
});
const customer = {
  name: '大阪太郎',
  age: undefined,
  followerIds: [1, 4, 7, 9],
};


/**
 * ZodNumber{spa: ƒ, _def: {…}, parse: ƒ, safeParse: ƒ, …}
 */
console.log(CustomerSchema.shape.followerIds.element);

空配列を許容したくない場合は、nonemptyが使用できます。

import { z } from 'zod';


const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
  followerIds: z.array(z.number()).nonempty(),
});
const customer = {
  name'大阪太郎',
  age: undefined,
  followerIds: [],
};


/**
 * Error: [
  {
    code: 'too_small',
    minimum: 1,
    type: 'array',
    inclusive: true,
    exact: false,
    message: 'Array must contain at least 1 element(s)',
    path: ['followerIds'],
  },
];


 */
console.log(CustomerSchema.parse(customer));

文字列型の高度なオプション

文字列長

const CustomerSchema = z.object({
  name: z.string().min(6).max(12),
  age: z.number().optional(),
});

メールアドレス形式

const CustomerSchema = z.object({
  name: z.string().min(6).max(12),
  email: z.string().email(),
});

url形式

import { z } from 'zod';


const CompanySchema = z.object({
  siteUrl: z.string().url(),
});
const c1 = { siteUrl: 'https://maasaablog.com/' };


/**
 * OK
 * {siteUrl: "https://maasaablog.com/"}
 */
console.log(CompanySchema.parse(c1));


const c2 = { siteUrl: 'maasaablog.com/' };


/**
 * NG
 * Error: [
    {
     validation: 'url',
     code: 'invalid_string',
     message: 'Invalid url',
     path: ['siteUrl'],
    },
  ];
 */
console.log(CompanySchema.parse(c2));

などなどほかにもたくさんあります。

数値型の高度なオプション

〇〇より大きい、以上、未満など

  • gt, grater than, >
  • gte, grater than equal, >=
  • lt, less than, <
  • lte, less than equal, <=
import { z } from 'zod';


const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().gt(19),
});
const customer = { name'大阪太郎', age: 20 };


/**
 * {name"大阪太郎", age: 20}
 */
console.log(CustomerSchema.parse(customer));

リテラル

リテラル型を使用したい場合は、literalを使用します。以下の例では会員の名称がの京都花子という文字列リテラル以外は受け付けなくなっています。

import { z } from 'zod';


const CustomerSchema = z.object({
  name: z.literal('京都花子'),
  age: z.number().nullish(),
});
const c1 = { name'京都花子', age: undefined };


/**
 * OK
 * {name"京都花子", age: undefined}
 */
console.log(CustomerSchema.parse(c1));


const c2 = { name'大阪太郎', age: undefined };
/**
 * NG
 * Error: [
  {
    received: '大阪太郎',
    code: 'invalid_literal',
    expected: '京都花子',
    path: ['name'],
    message: 'Invalid literal value, expected "京都花子"',
  },
];
 */
console.log(CustomerSchema.parse(c2));

なお、TypeScriptのEnumを使用して実装したい場合は、nativeEnumを使用します

ユニオン

unionを使用する

TypeScriptでもリテラルと組み合わせて使用するケースが多いのでその場合はunionの引数の配列の要素として、literalを使用して定義します。

import { z } from 'zod';


const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
  rank: z.union([z.literal('regular'), z.literal('premium')]),
});
const customer = {
  name'大阪太郎',
  age: undefined,
  rank: 'regular',
};


/**
 * {name"大阪太郎", age: undefined, rank: "regular"}
 */
console.log(CustomerSchema.parse(customer));

列挙型

列挙型は、enumを使用します。

import { z } from 'zod';
const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
  rank: z.enum(['regular', 'premium']),
});
const customer = { name: '大阪太郎', age: undefined, rank: 'regular' };


/**
 * {name: "大阪太郎", age: undefined, rank: "regular"}
 */
console.log(CustomerSchema.parse(customer));

タプル

tupleを使用する

import { z } from 'zod';


const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
  something: z.tuple([z.number(), z.string(), z.boolean()]),
});
const customer = {
  name: '大阪太郎',
  age: undefined,
  something: [1, '2', false],
};


/**
 * age: undefined
  name: "大阪太郎"
  something: Array[3]
    0: 1
    1: "2"
    2: false
 */
console.log(CustomerSchema.parse(customer));

また、restを使用することにより、一括でスキーマの型を指定可能です

import { z } from 'zod';


const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
  something: z.tuple([z.number(), z.string()]).rest(z.boolean()),
});
const customer = {
  name: '大阪太郎',
  age: undefined,
  something: [1, '2', false, false, true, false, false, false],
};


/**
 * age: undefined
  name: "大阪太郎"
  something: Array[8]
    0: 1
    1: "2"
    2: false
    3: false
    4: true
    5: false
    6: false
    7: false
 */
console.log(CustomerSchema.parse(customer));

レコード型

TypeScriptのレコード型(Record<Keys, Type>)をスキーマで表現するためにはrecordを使用します。

第一引数に、キーのデータ型を第二引数にバリューのデータ型を指定します

import { z } from 'zod';


const ProductIdSchema = z.record(z.string(), z.number());


/**
 * {id1}
 */
console.log(ProductIdSchema.parse({ id1 }));

任意項目

デフォルトでは、すべて必須(required)となります。任意項目としたい場合は、optionalを使用します

import { z } from 'zod';


const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().optional(),
  isLogin: z.boolean(),
});
const customer = { name'大阪太郎', isLogin: true };


/**
 * {name"大阪太郎", isLogin: true}
 */
console.log(CustomerSchema.parse(customer));

null許容

プロパティとしては必須ですが、その値にnullを許容する場合はnullableを使用します

import { z } from 'zod';


const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullable(),
});
const customer = { name: '大阪太郎', age: null };


/**
 * {name: "大阪太郎", age: null}
 */
console.log(CustomerSchema.parse(customer));

nullまたはundefined許容

プロパティとしては必須ですが、その値にnullまたはundefinedを許容する場合はnullishを使用します

import { z } from 'zod';


const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
});
const customer = { name: '大阪太郎', age: undefined };


/**
 * {name: "大阪太郎", age: undefined }
 */
console.log(CustomerSchema.parse(customer));

デフォルト値

デフォルト値を指定したい場合は、defaultを使用します

import { z } from 'zod';


const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
  rank: z.enum(['regular''premium']).nullish().default('regular'),
});
const customer = { name'大阪太郎', age: undefined };


/**
 * {name"大阪太郎", age: undefined, rank: "regular"}
 */
console.log(CustomerSchema.parse(customer));

その他

undefined、any型などのプリミティブも指定可能

const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().optional(),
  isLogin: z.boolean(),
  a: z.undefined(),
  b: z.null(),
  c: z.void(),
  d: z.any(),
  e: z.unknown(),
  f: z.never(),
});

他にも、mapやset、promiseなどもあります。

定義済オブジェクトスキーマの取得/加工

一部のスキーマのみ取得

HoheSchema.shape.fooの形式で、定義済スキーマから一部のスキーマのみ取得することができる。

import { z } from 'zod';
const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
  rank: z.enum(['regular''premium']),
});
const customer = { name'大阪太郎', age: undefined, rank: 'regular' };


/**
 * ZodString{spa: ƒ, _def: {…}, parse: ƒ, safeParse: ƒ, …}
 */
console.log(CustomerSchema.shape.name);

全てのスキーマのプロパティをオプショナルにする

partialを使用すると、すべてのプロパティをオプショナルにすることができる(プロパティ自体がなくてもバリデーションが成功する

import { z } from 'zod';
const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
  rank: z.enum(['regular', 'premium']),
});
const customer = { name: '大阪太郎', age: undefined, rank: 'regular' };


/**
 * age: ZodOptional name: ZodOptional rank: ZodOptional
 */
console.log(CustomerSchema.partial().shape);

ネストされたオブジェクトのスキーマであれば、deepPartialを使用する

一部のプロパティを取得してスキーマを再定義

pickを使用する

import { z } from 'zod';
const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
  rank: z.enum(['regular''premium']),
});
const customer = { name'大阪太郎', age: undefined, rank: 'regular' };


/**
 * name: ZodString
 */
console.log(CustomerSchema.pick({ nametrue }).shape);

一部のプロパティを除外してスキーマを再定義

omitを使用する

import { z } from 'zod';
const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
  rank: z.enum(['regular''premium']),
});
const customer = { name'大阪太郎', age: undefined, rank: 'regular' };


/**
 * {age: {…}, rank: {…}}
 */
console.log(CustomerSchema.omit({ nametrue }).shape);

スキーマの拡張

extendを使用する

import { z } from 'zod';
const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
  rank: z.enum(['regular''premium']),
});
const customer = { name'大阪太郎', age: undefined, rank: 'regular' };


/**
 * {name: {…}, age: {…}, rank: {…}, birthDay: {…}}
 */
console.log(CustomerSchema.extend({ birthDay: z.date() }).shape);

スキーマのマージ

mergeを使用すると、異なる2つのスキーマをマージすることが可能です

import { z } from 'zod';


const PersonSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
});


const RankSchema = z.object({
  rank: z.enum(['regular', 'premium']),
  applyDate: z.date(),
});


const CustomerSchema = PersonSchema.merge(RankSchema);


/**
 * {name: {…}, age: {…}, rank: {…}, applyDate: {…}}
 */
console.log(CustomerSchema.shape);

スキーマのカスタマイズ

refineを使用することで、スキーマの高度なカスタマイズが可能

import { z } from 'zod';


const CustomerSchema = z.object({
  name: z.string(),
  age: z.number().nullish(),
  email: z
    .string()
    .email()
    .refine((value) => value.endsWith('@gmail.com'), {
      message: 'Email must end with @gmail.com',
    }),
});
const c1 = {
  name'大阪太郎',
  age: undefined,
  email: 'sample@gmail.com',
};


/**
 * OK
 * {name"大阪太郎", age: undefined, email: "sample@gmail.com"}
 */
console.log(CustomerSchema.parse(c1));


const c2 = {
  name'大阪太郎',
  age: undefined,
  email: 'sample@hoge.com',
};


/**
 * NG
 * Error: [
  {
    code: 'custom',
    message: 'Email must end with @gmail.com',
    path: ['email'],
  },
];
 */
console.log(CustomerSchema.parse(c2));

TypeScriptの型をスキーマに依存させる

スキーマ定義から型を生成する

バリデーション用のスキーマを作ってから、その内容をもとにTypeScriptの型を生成できます。これはZodの非常に強力な機能の1つです。

具体的には、例えば以下のようにCustomer型を作成したい場合、Customerスキーマを生成後、z.inferを使用してその型を生成することが可能です

const CustomerSchema = z.object({
  id: z.number(),
  name: z.string().max(25),
  email: z
    .string()
    .email(),
});

type Customer = z.infer<typeof CustomerSchema>;

const customer: Customer = { id: 1, name: '会員A', email: 'test@test.com' };
CustomerSchema.parse(customer);

transformを使用した高度な型生成

スキーマから型を生成する際にtransformを使用すると、元のスキーマに特定のプロパティを追加した状態で型を生成することが可能です

例えば、フォーム上で会員の姓名が個別に入力されるような形式の場合に、transformを使用すると、元のスキーマにフルネームのプロパティを追加して型生成する等の実装を容易に行うことが可能です。

import { z } from 'zod';


const CustomerSchema = z
  .object({
    id: z.number(),
    lastName: z.string(),
    firstName: z.string(),
    email: z.string().email({ message: 'Invalid format email' }),
  })
  .transform((original) => {
    return {
      fullName: `${original.lastName} ${original.firstName}`,
      ...original,
    };
  });

/**
 * type Customer = {
    id?: number;
    lastName?: string;
    firstName?: string;
    email?: string;
    fullName: string;
}
 */
type Customer = z.infer<typeof CustomerSchema>;

エラーハンドリング

例外が投げられた場合、zodErrorのissuesにアクセスすることでエラー内容を取得できます。

issuesの中身は、以下のようなZodIssue型のオブジェクトとなっており、issues[0].messageとすると、エラーメッセージを取得することが可能となる(実際にはissuesの中身をループして表示したりするかと)

code: "unrecognized_keys"
keys: ['product_id']
message: "Unrecognized key(s) in object: 'product_id'"
path: []

また、実運用時は、送出されたErrorがZodErrorかどうかinstanceofで確認して、ZodErrorなら、issuesを取り出してループして表示する等が想定されます

const CustomerSchema = z.object({
  id: z.number(),
  name: z.string().max(25),
  email: z.string().email(),
});

type Customer = z.infer<typeof CustomerSchema>;

try {
  const customer: Customer = { id: 1, name: '会員A', email: 'testtstest.com' }; //@がない
  const success = CustomerSchema.parse(customer);
} catch (error) {
  if (error instanceof ZodError) {
    //error.issues
  }
}

ただ、上記の工程はやや面倒なので、zod-validation-errorというライブラリを用いると便利です。

独自のバリデーションメッセージを設定する

一部のオプションでは、引数にカスタマイズしたエラー文を渡すことが出来ます

例えば、string型の値に対して、以下のように型と必須エラーを指定できます。

公式より引用

const name = z.string({ 
  required_error: "名前は必須です", 
  invalid_type_error: "名前は文字列型である必要があります", 
});

他にも、stringの regex は、もともとは、"message": "Invalid",という簡素なメッセージなわけですが、第二引数にメッセージを指定することが可能です。

import { z } from 'zod';

const regExp = /^[0-9]{3}-[0-9]{4}$/g;
const ZipCodeSchema = z.string().regex(regExp, { message: 'Invalid format zip code' });

const failed = ZipCodeSchema.parse('5000001'); // "message": "Invalid format zip code"

regex以外にも、メッセージを指定可能なオプションは複数あります。(詳細は以下参照ください)

無関係なプロパティを禁止する

デフォルトでは、スキーマは、解析中に認識されないキーを取り除いて検証成功とします。

具体的には、以下のようにCustomerSchemaに関係のないproduct_idというプロパティがある場合、デフォルトの設定ではこれを除いた値がバリデーションをクリアすれば検証成功とします。

.strictを使う事で、検証時に無関係なプロパティがあった場合に検証失敗とするように出来ます

const CustomerSchema = z.object({
  id: z.number(),
  name: z.string().max(25),
  email: z.string().email(),
});

try {
  const customer = { id: 1, name: '会員A', email: 'testt@test.com', product_id: 2 }; //here
  const success = CustomerSchema.strict().parse(customer);
} catch (error) {
  //・・・
}

以上です。

本記事で紹介したzodの機能は全体のほんの一部です。そのほかの機能はREADMEに詳細にまとまっていますので、そちらを参照ください

まとめ

いかがでしたでしょうか。本記事では、モダンフロントエンド開発に必須級のバリデーションライブラリZodを紹介しています。TypeScriptだけでは容易に実装できないような細かなバリデーションをzodを使用することで簡単に実現できます。また、依存関係もないのでプロジェクトに容易に導入できる点もおすすめです