zod
なんか最近流行っているらしいスキーマの定義とバリデーションのライブラリ。
基本はこんな感じ。
import { z, ZodError } from "zod";
// スキーマを定義する
const schema = z.object({
id: z.string().max(3),
name: z.string().nonempty(),
})
// バリデーションを行なう
try {
// バリデーションが成功したときは user にオブジェクトが代入される
const user = schema.parse({
id: "abcde",
name: "shiraishi"
})
} catch (error) {
console.log(error);
}
// スキーマから型を生成する
type User = z.infer<typeof schema>
// {
// id: string,
// name: string,
// }
バリデーションが失敗した場合はこんなに親切な ZodError
が throw されます。
ZodError: [
{
"code": "too_big",
"maximum": 3,
"type": "string",
"inclusive": true,
"message": "Should be at most 3 characters long",
"path": [
"id"
]
}
]
他にもいろいろなメッソドや型が利用可能です。公式ドキュメントに全て記載されています。
実際のプロジェクトでどう使う??
公式ドキュメントはとても詳しく書いてありますが、実際のプロジェクトでどんな感じで使うのかまでは書いていません。zod を紹介する Qiita や zenn の記事でも、言及するものが少なかったので簡単に書いてみます。
今の所ぱっと思いつくのは
- schema をそのまま使うパターン
- class でラップするパターン
です。
schema をそのまま使うパターン
ディレクトリ構成
例えば、Vue.js
などで使う場合は下記のようなディレクトリ構成になります。
src
├── components
├── pages
├── schemas
│ ├── user_form_schema.ts
│ └── user_schema.ts
:
userSchema
userSchema
は API のレスポンスなどから受け取った値を User
オブジェクトに変換し、そのスキーマを保証するためのものです。
import { z, ZodError } from "zod";
export const userSchema = z.object({
id: z.string().max(3),
name: z.string().nonempty(),
});
export type User = z.infer<typeof userSchema>;
userSchema
を利用する側は次の様になります。
async function getUser(id: string) {
const data = await api.get(`/users/${id}`);
return userSchema.parse(data);
}
userFormSchema
user_form_schema
は API リクエストを送る直前などにバリデーションチェックをするためのものです。
import { z, ZodError } from "zod";
export const userFormSchema = z.object({
name: z.string().nonempty(),
});
export type UserForm = z.infer<typeof userFormSchema>;
userFormSchema
を利用する側は次の様になります。(Vue.js
の methods
っぽく書いています。)
async function onSubmit(userForm: UserForm) {
try {
api.putUser(userFormSchema.parse(userForm));
} catch (error) {
if(error instanceof ZodError) {
this.errorMessage = error.issues[0].message;
};
};
};
メリット
- zod のインターフェースをフル活用できる
- シンプル
デメリット
-
User
にメソッドやゲッターを持たせることができない
class でラップするパターン
ディレクトリ構成
前述の例と同様にVue.js
などで使う場合は下記のようなディレクトリ構成になります。
src
├── components
├── forms
│ └── user_form.ts
├── models
│ └── user.ts
├── pages
:
User クラス
User
は API のレスポンスなどから受け取ったユーザのモデルクラスです。
import { z } from "zod";
export const schema = z.object({
id: z.string().max(3),
firstName: z.string().nonempty().max(10),
lastName: z.string().nonempty().max(10),
});
type Schema = z.infer<typeof schema>
export class User {
constructor(data: any) {
this.value = schema.parse(data);
};
value!: Schema;
get fullName() {
return `${this.value.firstName} ${this.value.lastName}`
}
};
User
を利用する側は次の様になります。
async function getUser(id: string) {
const data = await api.get(`/users/${id}`);
return new User(data);
}
// 各パラメータへのアクセス
console.log(user.value.id)
UserForm
UserForm
は API リクエストのパラメータを保持し、バリデーションを行なうクラスです。
import { z, ZodError } from "zod";
const schema = z.object({
firstName: z.string().nonempty().max(10),
lastName: z.string().nonempty().max(10),
})
type Schema = z.infer<typeof schema>
export class UserForm {
value: Partial<Schema> = {}
private internalError?: ZodError
get error(): ZodError {
// 参照ではなくコピーを返す
return _.cloneDeep(this.internalError)
}
validate() {
try {
const checkedValue = schema.parse(this.value)
} catch (error) {
if(error instanceof ZodError) {
this.internalError = error
}
}
}
}
User
を利用する側は次の様になります。
async function onSubmit(userForm: UserForm) {
userForm.validate();
this.error = userForm.error();
if (!this.error) {
api.putUser(userFormSchema.parse(userForm));
}
}
メリット
-
User
やUserForm
にメソッドやゲッターを持たせることができる
デメリット
- これは本来想定されている zod の使い方なのか?と少し不安になる。
まとめ
実際のプロジェクトを意識して zod でスキーマを書いてみました。
ちょっと雑に書いてしまいましたので、
- こんな風に書くと良いよ!
- このプロダクトはイケてる書き方してるよ!
とかあれば是非教えてほしいです。