23
9

More than 1 year has passed since last update.

zod を実際のプロジェクトで使う時にどうするか考えた

Last updated at Posted at 2021-11-05

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 の記事でも、言及するものが少なかったので簡単に書いてみます。

今の所ぱっと思いつくのは

  1. schema をそのまま使うパターン
  2. class でラップするパターン

です。

schema をそのまま使うパターン

ディレクトリ構成

例えば、Vue.js などで使う場合は下記のようなディレクトリ構成になります。

src
├── components
├── pages
├── schemas
│   ├── user_form_schema.ts
│   └── user_schema.ts
:

userSchema

userSchema は API のレスポンスなどから受け取った値を User オブジェクトに変換し、そのスキーマを保証するためのものです。

user_schema.ts
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 リクエストを送る直前などにバリデーションチェックをするためのものです。

user_form_schema.ts
import { z, ZodError } from "zod";

export const userFormSchema = z.object({
  name: z.string().nonempty(),
});

export type UserForm = z.infer<typeof userFormSchema>;

userFormSchema を利用する側は次の様になります。(Vue.jsmethods っぽく書いています。)

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 リクエストのパラメータを保持し、バリデーションを行なうクラスです。

UserForm.ts
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));
  }
}

メリット

  • UserUserForm にメソッドやゲッターを持たせることができる

デメリット

  • これは本来想定されている zod の使い方なのか?と少し不安になる。

まとめ

実際のプロジェクトを意識して zod でスキーマを書いてみました。
ちょっと雑に書いてしまいましたので、

  • こんな風に書くと良いよ!
  • このプロダクトはイケてる書き方してるよ!

とかあれば是非教えてほしいです。

23
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
9