Zodとバリデーション/TypeScriptの基礎
Zod はバリデーションライブラリで入力フォームを通してユーザが入力する値が適切な値かどうかチェックするためのバリデーターとして利用することができます。入力フォームだけではなくバックエンドや localStorage から取得したデータが適切な値なのかチェックするために利用することもできます。つまり何かしら外部から取得したデータをアプリケーション内に取り込む際のバリデーションとして利用することができます。さらに Zod はバリデーションだけではなく TypeScript の型定義を利用する際に利用することもできます。
本文書では React を利用したフォームバリデーションとして Zod をどのように利用することができるのかを確認した後に TypeScript での型の作成方法について説明して Next.js を利用してバックエンドから取得したデータのチェックに Zod を利用して動作確認を行います。最後まで読み終える頃には Zod はフォームのバリデーションに利用するだけではなく受け取ったデータのチェックであればさまざなな場所で活用できることが理解できるようになります。
Zod は React のフォームライブラリの React Hook Form、Vue.js の VeeValidate で利用することもできます。どのようなフォームライブラリで利用できるかはZod Documentationで確認することができます。
React Hook Form での Zod の利用方法については下記の記事で公開しています。
zodはtRPCでも利用することができます。tRPCとは何か?またzodをどこで利用しているのか気になるはぜひ参考にしてみてください。
React でのフォームバリデーション
Zod にあまり馴染みがない人でも最初に Zod の名前を目にするのはフォームバリデーションでの利用だと思います。ここでは React の入力フォームを利用して Zod の動作確認を行うため React のプロジェクト、入力フォームの作成を行い、その後に Zod を利用してバリデーションの設定を行います。
プロジェクトの作成
create-react-app コマンドを利用して React のプロジェクトを作成します。ここでは react-zod-learn というプロジェクト名をつけていますが任意の名前をつけてください。
% npx create-react-app react-zod-learn
プロジェクトの作成後にreact-zod-learnに移動してください。
% cd react-zod-learn
フォームの作成
src フォルダにある App.js ファイルに Email とパスワードの入力項目を持つログインフォームのコードを記述します。
import './App.css';
import { useState } from 'react';
function App() {
const [data, setData] = useState({ email: '', password: '' });
const handleSubmit = (e) => {
e.preventDefault();
console.log(data);
};
const handleChange = (e) => {
const { name, value } = e.target;
setData({ ...data, [name]: value });
};
return (
<div className="App">
<h1>ログイン</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
value={data.email}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="password">パスワード</label>
<input
id="password"
name="password"
value={data.password}
onChange={handleChange}
type="password"
/>
</div>
<div>
<button type="submit">ログイン</button>
</div>
</form>
</div>
);
}
export default App;
App.jsファイルを更新後にnpm startコマンドを実行してlocalhost:3000にブラウザからアクセスすると以下のログインフォームが表示されます。
Email, パスワードを入力してログインボタンをクリックするとuseStateで定義したdataの内容がブラウザのデベロッパーツールのコンソールに表示されます。
{email: 'john@example.com', password: 'password'}
バリデーションによる入力値のチェックを行っていないため入力フォームに何も入れない場合でもメールアドレスの形式に合っていない値を入力してもログインボタンをクリックするとdataの内容が表示されます。
Zodによるバリデーション
Zodのインストール
Zodを利用してバリデーションを行うためにはZodライブラリのインストールが必要になります。
% npm install zod
バリデーションの設定
emailには必ず文字列の値を入力する必要がある場合にはZodを利用してスキーマを下記のように定義することでバリデーションを行うことができます。
import './App.css';
import { useState } from 'react';
import { z } from 'zod';
function App() {
const [data, setData] = useState({ email: '', password: '' });
const mySchema = z.string().min(1);
const handleSubmit = (e) => {
e.preventDefault();
mySchema.parse(data.email);
console.log(data);
};
//略
zodからimportしたzを利用してスキーマを定義しています。string()で文字列であること、min(1)で1文字以上であることを宣言しています。
const mySchema = z.string().min(1);
値のバリデーションによるチェックを定義したmySchemaのparseメソッドを利用し、引数には入力した値が入っているdata.emailを設定しています。
設定後、入力フォームの email に何かの値を入力してログインボタンをクリックするとバリデーションをパスして data の内容がコンソールに表示されます。しかし、email に何も入力せずにログインボタンをクリックするとバリデーションに失敗してコンソールには以下のメッセージが表示されます。
index.mjs:502 Uncaught ZodError: [
{
"code": "too_small",
"minimum": 1,
"type": "string",
"inclusive": true,
"exact": false,
"message": "String must contain at least 1 character(s)",
"path": []
}
]
at handleResult (index.mjs:502:1)
at ZodString.safeParse (index.mjs:615:1)
at ZodString.parse (index.mjs:595:1)
at handleSubmit (App.js:12:1)
at HTMLUnknownElement.callCallback (react-dom.development.js:4164:1)
at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:1)
at invokeGuardedCallback (react-dom.development.js:4277:1)
at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js:4291:1)
at executeDispatch (react-dom.development.js:9041:1)
at processDispatchQueueItemsInOrder (react-dom.development.js:9073:1)
エラーがthrowされるためconsole.logの処理は実行されません。
parse と safeParse
parse メソッドを利用した場合バリデーションに失敗するとエラーが throw されることがわかりました。バリデーションに成功した場合には parse メソッドの結果どのような値が戻されるか確認します。
const handleSubmit = (e) => {
e.preventDefault();
const result = mySchema.parse(data.email);
console.log(result);
console.log(data);
};
バリデーションにパスした場合には入力した data.email の値が戻り値に含まれることがわかります。フォームでメールアドレス(email) にjohn@example.comを入力した場合です。
john@example.com
バリデーションを行う方法には parse メソッドの他に safeParse メソッドがあります。parse メソッドから safeParse メソッドに変更することでどのような違いがあるか確認します。引数は parse と同じ入力した値を設定しますが戻り値が異なります。
const handleSubmit = (e) => {
e.preventDefault();
const result = mySchema.safeParse(data.email);
console.log(result);
console.log(data);
};
emailに値を入力した場合にはsafeParseから戻される値はオブジェクトとなりsuccessプロパティが含まれます。
{success: true, data: 'john@example.com'}
email に値を入力しない場合 parse メソッドではエラーが throw されましたが safeParse メソッドは success プロパティの値が false となり、error プロパティにエラーの情報が表示されます。
{success: false, error: ZodError: [
{
"code": "too_small",
"minimum": 1,
"type": "string",
"inclusive": t…}
safeParseメソッドを利用した場合はsuccessの値を利用してバリデーションにパスしたかどうかのチェックを行うことができます。
オブジェクトスキーマの作成
先ほどは email の入力値のみバリデーションを行いましたが入力フォームでは複数の入力値をチェックする必要があります。その場合にはオブジェクトスキーマを利用することができます。email にはメールアドレスの形式かどうかチェックが行える email()を設定しています。パスワードには文字列が 8 文字以上 32 文字以下であるかチェックを行えると min()と max()を設定しています。
const FormData = z.object({
email: z.string().email(),
password: z.string().min(8).max(32),
});
parseメソッドではdataオブジェクトを引数に設定します。
const handleSubmit = (e) => {
e.preventDefault();
FormData.parse(data);
console.log(data);
};
入力フォームに何も入力せず”ログイン”ボタンをクリックするとparseメソッドによりエラーがthrowされます。
index.mjs:502 Uncaught ZodError: [
{
"validation": "email",
"code": "invalid_string",
"message": "Invalid email",
"path": [
"email"
]
},
{
"code": "too_small",
"minimum": 8,
"type": "string",
"inclusive": true,
"exact": false,
"message": "String must contain at least 8 character(s)",
"path": [
"password"
]
}
]
//略
エラーメッセージの表示
parseメソッドによってバリデーションを行うことができますがブラウザ上にはエラーが表示されないためユーザにはどのようなエラーが発生しているのかがわかりません。入力値に問題があることをユーザに通知するためエラーの内容をブラウザ上に表示させます。
try, catchを利用してエラーの内容をコンソールに表示させます。throwされたエラーをflattenメソッドで取り出すことができます。
flattenメソッドなどのZodでのエラーのハンドリングについてはhttps://github.com/colinhacks/zod/blob/master/ERROR_HANDLING.mdに記載されています。
const handleSubmit = (e) => {
e.preventDefault();
try {
FormData.parse(data);
} catch (e) {
console.log(e.flatten());
}
console.log(data);
};
flattenメソッドを利用することでえらーの内容はfieldErrorsプロパティに含まれていることがわかります。
新たにuseStateでerrorsを定義してthrowされたエラーの内容を保存します。
import './App.css';
import { useState } from 'react';
import { z } from 'zod';
function App() {
const [data, setData] = useState({ email: '', password: '' });
const [errors, setErrors] = useState(null);
const FormData = z.object({
email: z.string().email(),
password: z.string().min(8).max(32),
});
const handleSubmit = (e) => {
e.preventDefault();
try {
FormData.parse(data);
console.log(data);
} catch (e) {
setErrors(e.flatten().fieldErrors);
}
};
//略
保存したerrorsの内容をブラウザ上に表示できるように設定を行います。
import './App.css';
import { useState } from 'react';
import { z } from 'zod';
function App() {
const [data, setData] = useState({ email: '', password: '' });
const [errors, setErrors] = useState(null);
const FormData = z.object({
email: z.string().email(),
password: z.string().min(8).max(32),
});
const handleSubmit = (e) => {
e.preventDefault();
try {
FormData.parse(data);
console.log(data);
} catch (e) {
setErrors(e.flatten().fieldErrors);
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setData({ ...data, [name]: value });
};
return (
<div className="App">
<h1>ログイン</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
value={data.email}
onChange={handleChange}
/>
</div>
{errors?.email && <div>{errors.email}</div>}
<div>
<label htmlFor="password">パスワード</label>
<input
id="password"
name="password"
value={data.password}
onChange={handleChange}
type="password"
/>
</div>
{errors?.password && <div>{errors.password}</div>}
<div>
<button type="submit">ログイン</button>
</div>
</form>
</div>
);
}
export default App;
設定後はブラウザ上にバリデーションのエラーが表示されるようになります。
ZodでthrowされるエラーはZodErrorオブジェクトのインスタンスかチェックを行いZodErrorオブジェクトのインスタンスの場合のみエラー設定を行います。
import { z, ZodError } from 'zod';
function App() {
const [data, setData] = useState({ email: '', password: '' });
const [errors, setErrors] = useState(null);
const FormData = z.object({
email: z.string().email(),
password: z.string().min(8).max(32),
});
const handleSubmit = (e) => {
e.preventDefault();
try {
FormData.parse(data);
console.log(data);
} catch (e) {
if (e instanceof ZodError) {
setErrors(e.flatten().fieldErrors);
} else {
console.log(e);
}
}
};
//略
エラーメッセージのカスタマイズ
デフォルトのエラーメッセージは英語なので日本語に変更することもできます。
const FormData = z.object({
email: z
.string()
.email({ message: 'メールアドレスの形式ではありません。' }),
password: z
.string()
.min(8, { message: '8文字以上入力してください。' })
.max(32, { message: '32文字以下で入力してください。' }),
});
設定後にエラーメッセージを確認すると設定したメッセージになっていることが確認できます。
safeParseによるエラーの設定
parse メソッドの場合はエラーが throw されるので try, catch を利用してエラーを取得しました。safeParse の場合は safeParse メソッドの戻り値に success プロパティが含まれることを確認したので success プロパティの値を分岐に利用することでエラーを設定することができます。
const handleSubmit = (e) => {
e.preventDefault();
const result = FormData.safeParse(data);
const errors = result.success ? {} : result.error.flatten().fieldErrors;
setErrors(errors);
console.log(data);
};
TypeScriptの環境構築
Viteを利用してTypeScriptを利用できる環境を構築します。Project nameには任意の名前をつけてください。frameworkでは”Vanilla”を選択し、variantでは”TypeScript”を選択します。
% npm create vite@latest
✔ Project name: … vite-zod-typescript
? Select a framework: › - Use arrow-keys. Return to submit.
❯ Vanilla
? Select a variant: › - Use arrow-keys. Return to submit.
JavaScript
❯ TypeScript
vite-zod-typescriptフォルダが作成されるので作成されるフォルダに移動してnpm installコマンドを実行します。
% cd vite-zod-typescript
% npm install
npm installコマンド完了後はnpm run devコマンドを実行して開発サーバを起動します。開発サーバ起動はブラウザのデベロッパーツールのコンソールを開きます。
Zodのインストール
Typescript環境でZodを利用するためにはZodライブラリのインストールが必要となります。
% npm install zod
srcフォルダにあるmain.tsファイルを更新することでZodの動作確認を行なっていきます。
TypeScriptでのZodの利用
parseメソッドの実行
バリデーションの説明で Zod の parse メソッドの利用方法を理解できていると思うので main.ts ファイルを利用して Zod の parse メソッドを実行します。オブジェクトスキーマを定義しています。
import { z } from 'zod';
const UserSchema = z.object({
username: z.string(),
});
const user = { username: 'John' };
const result = UserSchema.parse(user);
console.log(result);
npm run devコマンドを実行している場合はファイルの更新を行うと自動で反映されコンソールにresultの値が表示されます。parseメソッドでは引数に設定したusernameプロパティの値が文字列の”John”なのでバリデーションをパスするのでparseメソッドで指定したオブジェクトが表示されます。
{username: 'John'}
usernameの値を数値にするとバリデーションに失敗しエラーがthrowされます。
import { z } from 'zod';
const UserSchema = z.object({
username: z.string(),
});
const user = { username: 20 };
const result = UserSchema.parse(user);
console.log(result);
コンソールを確認するとZodErrorのメッセージが表示されます。
index.mjs:502 Uncaught ZodError: [
{
"code": "invalid_type",
"expected": "string",
"received": "number",
"path": [
"username"
],
"message": "Expected string, received number"
}
]
at handleResult (index.mjs:502:23)
at ZodObject.safeParse (index.mjs:615:16)
at ZodObject.parse (index.mjs:595:29)
at main.ts:7:21
TypeScriptの型を作成
TypeScriptではinterfaceかtypeを利用してオブジェクトの型を以下のように設定することができます。
import { z } from 'zod';
const UserSchema = z.object({
username: z.string(),
});
interface User {
username: string;
}
// or
type User = {
username: string;
}
const user: User = { username: 'John' };
const result = UserSchema.parse(user);
console.log(result);
Zodでは定義したオブジェクトスキーマを利用してTypeScriptのTypeを作成することができます。
import { z } from 'zod';
const UserSchema = z.object({
username: z.string(),
});
type User = z.infer<typeof UserSchema>;
const user: User = { username: 'John' };
const result = UserSchema.parse(user);
console.log(result);
type Userにカーソルを合わせるとUserの型が表示されます。
このようにZodで定義したスキーマを利用することでTypeScriptの型を作成することができます。
Next.js で Zod を利用してみよう
フォームバリデーション以外に Zod はどのような場所で利用することができるか Next.js を利用して確認していきます。
Next.js のプロジェクトの作成と Zod のインストールを行います。
% npx create-next-app@latest nextjs-zod-learn
任意の名前でプロジェクトを作成後に Zod のインストールを行います。
% npm install zod
バックエンドから戻されるデータでの利用
外部リソースから取得したデータが正しい形式のデータであるのかチェックを行うために Zod を利用することができます。Next.js では Route Handler を利用することができるので Router Handler を外部のリソースとして利用します。
src/app ディレクトリの下に app ディレクトリを作成して route.ts ファイルを作成して以下のコードを記述します。
export async function GET() {
const user = {
id: 1,
firstName: 'John',
lastName: 'Doe',
age: 30,
};
return Response.json(user);
}
components ディレクトリを作成して User.tsx ファイルを作成して User コンポーネントの中で http://localhost:3000/api にアクセスします。
components/User.tsx
'use client';
import { useEffect } from 'react';
type UserType = {
id: number;
firstName: string;
lastName: string;
age: number;
};
const User = () => {
useEffect(() => {
const getUser = async () => {
const response = await fetch('http://localhost:3000/api');
const user: UserType = await response.json();
console.log(user.firstName);
};
getUser();
});
return <div>User</div>;
};
export default User;
作成した User コンポーネントは app ディレクトの page.tsx ファイルで import します。
import User from './components/User';
export default function Home() {
return <User />;
}
ブラウザで確認するとコンソールには”John”が表示されます。 何かの拍子に route.ts 側で設定した firstName が firstname に変更されたとします。
export async function GET() {
const user = {
id: 1,
firstname: 'John',
lastName: 'Doe',
age: 30,
};
return Response.json(user);
}
ブラウザのコンソールには”undefined”が表示されます。もしこの時に Zod によりバリデーションを行っていたらどのようになるか確認します。スキーマの定義を行います。
'use client';
import { useEffect } from 'react';
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
firstName: z.string(),
lastName: z.string(),
age: z.number(),
});
//略
定義した UserSchema を利用して parse を実行します。
const User = () => {
useEffect(() => {
const getUser = async () => {
const response = await fetch('http://localhost:3000/api');
const user: UserType = await response.json();
const validatedUser = UserSchema.parse(user);
console.log(validatedUser.firstName);
};
getUser();
});
return <div>User</div>;
};
export default User;
ブラウザ側でアクセスすると先ほどまでは undefined と表示されましたが parse でエラーが発生すると Exception が throw されるので画面にエラーメッセージが表示されます。
Unhandled Runtime Error
ZodError: [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": [
"firstName"
],
"message": "Required"
}
]
route.ts の firstname を firstName に変更するか UserSchema 側で firstName を fristname にするとエラーは解消します。
下記のように戻される値が数値ではなく文字列だった場合の動作確認も行います。
export async function GET() {
const user = {
id: 'eomoeam-1emowea',
firstName: 'John',
lastName: 'Doe',
age: 30,
};
return Response.json(user);
}
ブラウザ上には下記のエラーが表示されます。
Unhandled Runtime Error
ZodError: [
{
"code": "invalid_type",
"expected": "number",
"received": "string",
"path": [
"id"
],
"message": "Expected number, received string"
}
]
zod を利用していない場合では UsetType で id の型を number にしていても特にエラーは表示されずバックエンドから戻された id が文字列として表示されます。
'use client';
import { useEffect } from 'react';
type UserType = {
id: number;
firstName: string;
lastName: string;
age: number;
};
const User = () => {
useEffect(() => {
const getUser = async () => {
const response = await fetch('http://localhost:3000/api');
const user: UserType = await response.json();
// const validatedUser = UserSchema.parse(user);
// console.log(validatedUser.id);
console.log(user.id);
};
getUser();
});
return <div>User</div>;
};
export default User;
このように外部リソースから取得したデータが正しい形式のデータかどうかを Zod を利用してチェックすることができます。
フォームのバリデーションという狭い範囲に限らずデータを取得する場所ならどこでも利用できることがわかったかと思います。ぜひさまざまな場所で Zod を利用してみてください。