😊

Prisma開発者必見!zod-prisma-typesが生成する3つのスキーマの使い方

2024/09/15に公開

1. はじめに

  • 本記事では、PrismaとZodを組み合わせたzod-prisma-typesを使用して、型安全なバリデーションを実装する方法を解説します。
  • 特に、zod-prisma-typesが自動生成する3つのスキーマ(inputTypeSchemas、modelSchema、outputTypeSchemas)に焦点を当て、関連テーブルを含むデータのバリデーションと操作方法を詳しく説明します。
  • 対象バージョンはv3.1.8

https://github.com/chrishoermann/zod-prisma-types

対象読者

  • Prismaの基本を理解している初級から中級レベルの開発者
  • zod-prisma-typesの効果的な活用方法を学びたい方

2. zod-prisma-typesのセットアップと基本的な使用方法

本記事のディレクトリ構造

記事で言及するディレクトリとファイルは以下の通りです。

project-root/
├── prisma/
│   ├── schema.prisma
│   └── generated/
│       └── zod/
│           ├── inputTypeSchemas/
│           │   ├── UserCreateInputSchema.ts
│           │   └── PostCreateInputSchema.ts
│           ├── modelSchema/
│           │   ├── UserSchema.ts
│           │   └── PostSchema.ts
│           └── outputTypeSchemas/
│               ├── UserArgsSchema.ts
│               └── UserCreateArgsSchema.ts
  • prisma/schema.prisma: Prismaのスキーマ定義ファイル
  • prisma/generated/zod/: zod-prisma-typesによって生成されるZodスキーマのディレクトリ
    • inputTypeSchemas/: データベース入力操作(作成、更新)するためのスキーマ
    • modelSchema/: データベースモデル全体の構造を表現するためのスキーマ
    • outputTypeSchemas/: データベースからのクエリ結果の形状を定義するスためのキーマ

2.1 プロジェクトのセットアップ

  1. 必要なパッケージのインストール
npm install zod zod-prisma-types @prisma/client
  1. prisma/schema.prisma ファイルに以下の generator を追加
generator client {
  provider = "prisma-client-js"
}

generator zod {
  provider          = "zod-prisma-types"
  useMultipleFiles  = true
  writeBarrelFiles  = false
  useTypeAssertions = true
}

この設定で、Prisma スキーマから自動的に Zod スキーマを生成します。

  1. スキーマ生成コマンドの実行
npx prisma generate

2.2 Prismaスキーマの定義

ユーザー(User)と投稿(Post)の関連を持つブログを例に、Prismaスキーマを定義します。

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  author    User    @relation(fields: [authorId], references: [id])
  authorId  Int
}

このスキーマの主なポイントは以下の通りです。

  1. UserモデルとPostモデルを定義
  2. UserPost一対多の関係
  3. Postは必ず一人のUser(著者)に属する

2.3 生成されたスキーマの使用

zod-prisma-typesが生成したZodスキーマは@/prisma/generated/zodに保存されます。
これらのスキーマを使用してデータのバリデーションを行います。ここでは、omitメソッドとsafeParseメソッドを使用した例を示します。

import { z } from 'zod';
import { UserSchema, PostSchema } from '@/prisma/generated/zod/modelSchema';

// omitメソッドを使用して入力用のスキーマを定義
// omit: 指定したフィールドを除外した新しいスキーマを作成する
// これにより、自動生成されるidやデータベース側で管理される関連フィールドを除外できる

// UserSchemaからidフィールドを除外した入力用スキーマを作成
const UserInputSchema = UserSchema.omit({ id: true });

// PostSchemaからidとauthorIdフィールドを除外した入力用スキーマを作成
// authorIdは関連テーブルの操作時に別途処理するため、ここでは除外する
const PostInputSchema = PostSchema.omit({ id: true, authorId: true });

// ユーザー作成データのバリデーション例
const validUserInput = {
  email: "user@example.com",
  name: "John Doe"
};

const invalidUserInput = {
  email: "invalid-email",
  name: 123  // 不正な型
};

// 投稿作成データのバリデーション例
const validPostInput = {
  title: "新しい投稿",
  content: "これは新しい投稿の内容です。",
  published: true
};

const invalidPostInput = {
  title: "",  // 空のタイトル
  content: "内容",
  published: "yes"  // 不正な型
};

// バリデーション実行
const validUserResult = UserInputSchema.safeParse(validUserInput);
const invalidUserResult = UserInputSchema.safeParse(invalidUserInput);
const validPostResult = PostInputSchema.safeParse(validPostInput);
const invalidPostResult = PostInputSchema.safeParse(invalidPostInput);
  1. omitメソッド

    • 不要なフィールドを除外した新しいスキーマの作成できるメソッド
      • UserSchema.omit({ id: true })
        • idフィールドを除外した新しいスキーマを作成
      • PostSchema.omit({ id: true, authorId: true })
        • idauthorIdフィールドを除外
  2. safeParseメソッド

    • バリデーション実行と結果オブジェクトの返却するメソッド
      • 成功時は{ success: true, data: validatedData }を返却
      • 失敗時は{ success: false, error: ZodError }を返却

バリデーションが成功した場合の処理

if (validUserResult.success) {
  console.log("有効なユーザーデータ:", validUserResult.data);
  // ログ出力: 有効なユーザーデータ: { email: "user@example.com", name: "John Doe" }
  
  // validUserResult.data は型安全
  const email: string = validUserResult.data.email;
  const name: string | undefined = validUserResult.data.name;
  
  // このデータを使用してデータベース操作などを行う
  // 例: await prisma.user.create({ data: validUserResult.data });
}

if (validPostResult.success) {
  console.log("有効な投稿データ:", validPostResult.data);
  // ログ出力: 有効な投稿データ: { title: "新しい投稿", content: "これは新しい投稿の内容です。", published: true }
  
  // validPostResult.data は型安全
  const title: string = validPostResult.data.title;
  const content: string | undefined = validPostResult.data.content;
  const published: boolean = validPostResult.data.published;
}

バリデーションエラーの処理

if (!invalidUserResult.success) {
  console.error("ユーザーデータのバリデーションエラー:");
  invalidUserResult.error.issues.forEach(issue => {
    console.error(`- ${issue.path.join('.')}: ${issue.message}`);
  });
  // ログ出力:
  // ユーザーデータのバリデーションエラー:
  // - email: Invalid email
  // - name: Expected string, received number
}

if (!invalidPostResult.success) {
  console.error("投稿データのバリデーションエラー:");
  invalidPostResult.error.issues.forEach(issue => {
    console.error(`- ${issue.path.join('.')}: ${issue.message}`);
  });
  // ログ出力:
  // 投稿データのバリデーションエラー:
  // - title: String must contain at least 1 character(s)
  // - published: Expected boolean, received string
}
  • errorプロパティにZodErrorオブジェクトを格納
    • error.issues配列に具体的なバリデーションエラーの詳細情報を格納
      • 各issueにpath(エラーが発生したプロパティ)、message(エラーメッセージ)、code(エラーコード)を格納

まとめ

  • omitメソッド
    • クライアントサイドでの入力データの適切なバリデーションを実行
    • 例:自動生成されるidフィールドや関連フィールド(authorId)を除外したスキーマを作成
  • safeParseメソッド
    • バリデーション結果をsuccessプロパティに格納し、成功と失敗を分類
      • 成功時(success: true):
        • dataプロパティに型安全なバリデーション済みデータを格納
        • このデータを直接使用可能
      • 失敗時(success: false):
        • errorプロパティに詳細なエラー情報を格納
        • error.issues配列に具体的なバリデーションエラーの詳細情報を格納
        • 各issueにpath(エラーが発生したプロパティ)、message(エラーメッセージ)、code(エラーコード)などの情報を格納
        • これらの情報を活用して入力フォームのエラーメッセージ表示や例外処理を実装

3. zod-prisma-typesが生成する3つのスキーマ

  • zod-prisma-typesは、schema.prismaから3つのZodスキーマを自動生成します。
  • 各スキーマの特徴、役割、および使用方法について解説します。

注意
schema.prismaの設定を以下のように設定してください。

generator zod {
  provider          = "zod-prisma-types"
  useMultipleFiles  = true
  writeBarrelFiles  = false
  useTypeAssertions = true
}

3つのZodスキーマ

  1. inputTypeSchemas: データベース入力操作(作成、更新)するためのスキーマ
  2. modelSchema: データベースモデル全体の構造を表現するためのスキーマ
  3. outputTypeSchemas: データベースからのクエリ結果の形状を定義するスためのキーマ

3.1 inputTypeSchemas

prisma/generated/zod/inputTypeSchemasに出力

概要と役割

  • データベースへの入力操作(作成、更新)に使用
  • データの作成(create)や更新(update)時のバリデーション
  • リクエストボディの検証
  • 必須フィールドと任意フィールドの指定
  • 関連テーブルの操作(connect, create, update, upsert など)

使用例

import { PostCreateInputSchema, UserCreateInputSchema } from '@/prisma/generated/zod/inputTypeSchemas';

// 新しい投稿を作成し、既存のユーザーと関連付ける
const createPostWithExistingUser = PostCreateInputSchema.parse({
  title: "新しい投稿",
  content: "これは新しい投稿の内容です。",
  author: {
    connect: { id: existingUserId }
  }
});

// 新しいユーザーを作成し、同時に新しい投稿も作成して関連付ける
const createUserWithNewPost = UserCreateInputSchema.parse({
  name: "新しいユーザー",
  email: "new@example.com",
  posts: {
    create: [
      {
        title: "新しいユーザーの最初の投稿",
        content: "これは新しいユーザーが作成した最初の投稿です。"
      }
    ]
  }
});
  1. PostCreateInputSchema.parse()
    • 新しい投稿データをバリデーション
    • author.connectで既存のユーザーとの関連付けを指定
  2. UserCreateInputSchema.parse()
    • 新しいユーザーデータをバリデーション
    • posts.createで同時に新しい投稿を作成し関連付け
  3. 関連テーブルの操作
    • connect

      • 既存のレコードの関連付けに使用
      • 例:
        author: { 
          connect: { 
            id: existingUserId 
          } 
        }
        
      • この例では既存のユーザーを新しい投稿に関連付け
    • create

      • 関連する新しいレコードの作成と同時に関連付けを実行
      • 例:
        posts: { 
          create: [
            {
              title: "新しい投稿",
              content: "これは新しい投稿の内容です。"
            }
          ] 
        }
        
      • この例では新しいユーザーを作成すると同時に、関連する投稿も作成
    • update

      • 関連するレコードの更新
      • 例:
        posts: { 
          update: { 
            where: { id: postId }, 
            data: { 
              title: "更新されたタイトル" 
            } 
          } 
        }
        
    • upsert

      • レコードが存在しない場合は作成し、存在する場合は更新
      • 例:
        posts: { 
          upsert: { 
            where: { id: postId }, 
            update: { title: "更新" }, 
            create: { title: "新規作成" } 
          } 
        }
        

Tips

  • これらの操作は、Prismaの機能を反映したZodスキーマによって型安全に実行可能
  • スキーマは、Prismaで定義された関係性(一対多、多対多など)を正確に反映

3.2 modelSchema

prisma/generated/zod/modelSchemaに出力

概要と役割

  • データベースモデル全体の構造を表現
  • データベースから取得したデータの検証
  • モデルの完全な構造の定義
  • 全てのフィールド(主キー、外部キーを含む)の型チェック

使用例

import { UserSchema } from '@/prisma/generated/zod/modelSchema';

// データベースから取得したユーザーデータの検証
const user = await prisma.user.findUnique({ where: { id: userId } });
const validatedUser = UserSchema.parse(user);

console.log("検証済みユーザー:", validatedUser);
// 出力例: { id: 1, email: "user@example.com", name: "John Doe", posts: [...] }

// 型安全な操作
const userName: string | null = validatedUser.name;
const userEmail: string = validatedUser.email;
  1. UserSchema.parse(user)
    • データベースから取得したユーザーデータを検証
    • 全てのフィールド(id, email, name, posts)が正しい型であることを確認
    • 関連するpostsデータも含めた検証(取得している場合)
  2. 型安全な操作
    • validatedUserオブジェクトを使用した型安全な操作が可能
      • TypeScriptの型チェックを活用し、潜在的なエラーを事前に防止
    • 例:
      const userName: string | null = validatedUser.name;
      const userEmail: string = validatedUser.email;
      
    • userNamestring | null型として推論され、名前が設定されていない可能性を考慮
    • userEmailstring型として推論され、常に値が存在することを保証

3.3 outputTypeSchemas

prisma/generated/zod/outputTypeSchemasに出力

概要と役割

  • データベースからのクエリ結果の形状を定義
  • クエリ引数の型定義と検証
  • リレーションを含むクエリ結果の検証
  • クライアントに返すデータ構造の定義

使用例

import { UserArgsSchema, UserCreateArgsSchema } from '@/prisma/generated/zod/outputTypeSchemas';
import { z } from 'zod';

// クエリ引数の型定義と検証
const userQuerySchema = UserArgsSchema.pick({
  where: true,
  select: true,
  include: true
});

const userQueryInput = {
  where: { id: 1 },
  select: { id: true, name: true, email: true },
  include: { posts: true }
};

const validatedUserQueryArgs = userQuerySchema.parse(userQueryInput);

// 上記の検証済み引数を使用してクエリを実行
const user = await prisma.user.findUnique(validatedUserQueryArgs);

// リレーションを含むクエリ結果の検証
const userWithPostsSchema = z.object({
  id: z.number(),
  name: z.string().nullable(),
  email: z.string(),
  posts: z.array(z.object({
    id: z.number(),
    title: z.string(),
    content: z.string().nullable(),
    published: z.boolean()
  }))
});

const userWithPosts = await prisma.user.findUnique({
  where: { id: 1 },
  include: { posts: true }
});

const validatedUserWithPosts = userWithPostsSchema.parse(userWithPosts);

// ユーザー作成時の引数の検証
const createUserInput = {
  data: {
    name: "新規ユーザー",
    email: "new@example.com",
    posts: {
      create: [
        { title: "最初の投稿", content: "これは新しいユーザーの最初の投稿です。" }
      ]
    }
  }
};

const validatedCreateUserArgs = UserCreateArgsSchema.parse(createUserInput);

// 検証済みの引数を使用してユーザーを作成
const newUser = await prisma.user.create(validatedCreateUserArgs);

console.log("新規ユーザー:", newUser);
// 出力例: { id: 2, name: "新規ユーザー", email: "new@example.com" }

説明

  1. クエリ引数の型定義と検証

    • UserArgsSchema.pick()を使用して、必要な引数のみを含むカスタムスキーマを作成
    • このスキーマを使用して、findUniqueなどのクエリメソッドの引数を検証
    • whereselectincludeオプションを含む引数構造の定義と検証
  2. リレーションを含むクエリ結果の検証

    • z.object()を使用して、ユーザーと関連する投稿を含むカスタムスキーマを定義
    • このスキーマを使用して、データベースから取得したユーザーと投稿のデータを検証
    • ネストされたデータ構造(ユーザーと関連する投稿)を正確に定義し、型安全性を確保
  3. ユーザー作成時の引数の検証

    • UserCreateArgsSchemaを使用して、ユーザー作成時の完全な引数オブジェクトを検証
    • ネストされたデータ構造(この場合は関連するpostsの作成)も含めて検証
    • 検証済みの引数を使用してデータベース操作を実行し、型安全性を維持

4. まとめ

この記事では、zod-prisma-typesを活用したバリデーションについて、特に、zod-prisma-typesが自動生成する3つのスキーマ(inputTypeSchemas、modelSchema、outputTypeSchemas)を解説しました。

主なポイントは以下の通りです。

  • zod-prisma-typesが生成する3つのスキーマの役割と使用方法
    1. inputTypeSchemas
      • 役割:データベースへの入力操作(作成、更新)のバリデーション
      • 使用方法:リクエストボディの検証、必須・任意フィールドの指定、関連テーブルの操作
      • 例:PostCreateInputSchema, UserCreateInputSchemaの使用
    2. modelSchema
      • 役割:データベースモデル全体の構造表現と検証
      • 使用方法:データベースから取得したデータの完全性チェック、全フィールドの型チェック
      • 例:UserSchemaを使用したデータベースから取得したユーザーデータの検証
    3. outputTypeSchemas
      • 役割:データベースクエリ結果の形状定義と検証
      • 使用方法:クエリ引数の型定義、リレーションを含むクエリ結果の検証、クライアントに返すデータ構造の定義
      • 例:UserArgsSchema, UserCreateArgsSchemaを使用したクエリ引数の検証
  • zod-prisma-typesの主要メソッドのまとめ
    1. omitメソッド
      • 用途:特定のフィールドを除外した新しいスキーマの作成
      • 例:UserSchema.omit({ id: true })idフィールドを除外
    2. safeParseメソッド
      • 用途:バリデーション結果の成功と失敗の判別、および適切な処理の実行
      • 特徴:
        • 成功時(success: true
          • dataプロパティに型安全なバリデーション済みデータを格納
        • 失敗時(success: false
          • errorプロパティに詳細なエラー情報を格納
            • error.issues配列にpathmessagecodeなどの詳細情報を格納
    3. parseメソッド
      • 用途:データの検証と型変換を行い、エラーがある場合は例外をスロー
      • 例:UserCreateArgsSchema.parse(createUserInput)でユーザー作成時の引数を検証
    4. 関連テーブル操作メソッド
      • connect:既存レコードの関連付け
      • create:新規レコードの作成と関連付け
      • update:関連レコードの更新
      • upsert:レコードの作成または更新
    5. pickメソッド
      • 用途:特定のフィールドのみを選択して新しいスキーマを作成
      • 例:UserArgsSchema.pick({ where: true, select: true, include: true })でクエリ引数のカスタムスキーマを作成

以上、最後までお読みいただきありがとうございました!

Discussion