こんにちは。株式会社 TechDoctor でソフトウェアエンジニアをしている大瀧です。
突然ですが、アプリケーション開発で、クライアントとサーバーの API の型やバリデーションルールが食い違い、予期せぬバグに繋がった経験はないでしょうか。
このような課題の解決策として、本記事では、OpenAPI スキーマをもとに、API の型とバリデーションルールを一元管理する方法を紹介します。
ここで紹介するアプローチは、 OpenAPI スキーマを作成できる限り特定の言語やフレームワークに依存しませんが、今回は具体例として FastAPI、Pydantic、Zod、openapi-zod-client を使った方法を解説します。
なお本記事のサンプルコードは、以下のリポジトリで確認できます。(Kiro と Claude Code が作ってくれました)
GitHub - shutootaki/sync-schema-demo
1. 背景と課題
課題 ①:API の型の二重管理
API のインターフェースの型が、バックエンドとフロントエンドでそれぞれ定義されていると、二重管理になってしまいます。
この状態では、仕様変更のたびに両方のコードを手動で同期させる必要が生まれます。
その際に片方で修正漏れが起きると、データの不整合や実行時エラーに直結します。結果として、ランタイムエラーが発生したり、「このフィールドは null 許容でしたっけ?」といった本来不要なコミュニケーションが頻発してしまいます。
課題 ②:バリデーションロジックの分散
優れた UX のためにはクライアント側での事前バリデーションが必要です。そしてデータの整合性とセキュリティのためにはサーバー側でのバリデーションも不可欠です。
しかし、これらのバリデーションルールをそれぞれ別のリポジトリで管理していると、いつの間にか仕様が乖離しがちです。
結果として、課題 ① と同様にエラーや不要なコミュニケーションコストが発生してしまいます。
2. 解決アプローチ
この課題を解決するために、弊社では以下のアプローチを採用しました。
バックエンドを信頼できる唯一の情報源(SSoT)にする
バックエンド(FastAPI + Pydantic)を、型とバリデーションルールにおける信頼できる唯一の情報源と位置づけます。
そのために、コーディング時はPydantic でリクエスト/レスポンスの型とバリデーションルールを定義します。
あとは自動的に同期が行われる仕組みにします。
3. 具体的な実装方法
基本的なユーザー作成 API を例に、具体的な実装方法を解説します。
① Pydantic モデルの定義と FastAPI によるドキュメント生成
バックエンドでは、Pydantic を使いリクエストとレスポンスのスキーマを定義します。
基本的なスキーマ定義
from pydantic import BaseModel, Field, EmailStr from typing import Optional from enum import Enum class UserRole(str, Enum): USER = "user" ADMIN = "admin" class UserCreateRequest(BaseModel): name: str = Field(..., min_length=1, max_length=100) email: EmailStr age: int = Field(..., ge=18, le=120) role: UserRole = UserRole.USER class UserResponse(BaseModel): id: int name: str email: str age: int role: UserRole
FastAPI ルーターでのスキーマの使用
定義した Pydantic スキーマを、FastAPI のエンドポイントで利用します。
from fastapi import APIRouter, HTTPException from app.models.user import UserCreateRequest, UserResponse import random router = APIRouter(tags=["Users"]) users_db: dict = {} @router.post("/users", response_model=UserResponse) async def create_user(user_data: UserCreateRequest) -> UserResponse: new_user = { "id": generate_user_id(), "name": user_data.name, "email": user_data.email, "age": user_data.age, "role": user_data.role, } users_db[new_user["id"]] = new_user return UserResponse.model_validate(new_user)
FastAPI アプリケーションの設定
main.py で、OpenAPI ドキュメント生成の設定を行います。
from fastapi import FastAPI from app.routers import users app = FastAPI( title="User API", version="1.0.0", description="シンプルなユーザー管理API" ) app.include_router(users.router, prefix="/api/v1")
生成される OpenAPI スキーマ
上記の Pydantic モデルと FastAPI の設定から、次のような OpenAPI スキーマが自動生成されます。(一部抜粋)
{ "openapi": "3.1.0", "info": { "title": "User API", "version": "1.0.0" }, "components": { "schemas": { "UserCreateRequest": { "properties": { "name": { "type": "string", "maxLength": 100, "minLength": 1, "title": "Name" }, "email": { "type": "string", "format": "email", "title": "Email" }, "age": { "type": "integer", "maximum": 120, "minimum": 18, "title": "Age" }, "role": { "$ref": "#/components/schemas/UserRole", "default": "user" } }, "title": "UserCreateRequest", "description": "ユーザー作成リクエストのスキーマ" }, "UserRole": { "type": "string", "enum": ["user", "admin"], "title": "UserRole" } } } }

このスキーマには、minLength=1 や ge=18 といったバリデーションルール、UserRole の Enum 定義、必須フィールドの指定、整数・文字列・メール形式といった詳細な型情報が含まれています。
② openapi-zod-clientによるスキーマファイルの生成
フロントエンド側では、FastAPI が生成した OpenAPI スキーマをもとに、openapi-zod-clientを使って Zod スキーマを自動生成します。
openapi-zod-clientのインストールと設定
まず、フロントエンドプロジェクトに必要なパッケージをインストールします。
pnpm install -D openapi-zod-client
次に、プロジェクトのルートに設定ファイルを作成します。
// schema-format.hbs
// This file is automatically generated. Do not change it manually.
import { z } from "zod";
{{#each schemas}}
export const {{@key}} = {{{this}}};
export type {{@key}} = z.infer<typeof {{@key}}>;
{{/each}}
export const schemas = {
{{#each schemas}}
{{@key}},
{{/each}}
};上記はシンプルな設定例です。詳細は公式ドキュメントを参照してください。
GitHub - astahmer/openapi-zod-client: Generate a zodios (typescript http client with zod validation) from an OpenAPI spec (json/yaml)
package.jsonへのスクリプト追加
開発ワークフローを効率化するため、package.json にスクリプトを追加します。
{ "scripts": { "codegen": "openapi-zod-client -o ./src/lib/api/schema/api_schemas.ts -t ./schema-format.hbs ${OPEN_API_JSON:-<http://localhost:8000/openapi.json>}" } }
バックエンドサーバーが起動している状態で以下のコマンドを実行すると、Zod スキーマが自動で生成されます。
pnpm run codegen
実際には以下のような Zod スキーマが自動で生成されます。(一部抜粋)
OpenAPI スキーマをもとに UserCreateRequest の型とバリデーションルールが、Zod のスキーマに変換されています。
// src/lib/api/schema/api_schemas.ts // This file is automatically generated. Do not change it manually. import { z } from "zod"; export const UserRole = z.enum(["user", "admin"]); export type UserRole = z.infer<typeof UserRole>; export const UserCreateRequest = z .object({ name: z.string().min(1).max(100), email: z.string().email(), age: z.number().int().gte(18).lte(120), role: UserRole.optional(), }) .passthrough(); export type UserCreateRequest = z.infer<typeof UserCreateRequest>;
React Hook Form との連携
生成された Zod スキーマは、React Hook Form のようなフォームの状態管理をするライブラリと簡単に連携できます。
以下のように、src/lib/api/schema/api_schemas.ts に出力された Zod スキーマを import して、useForm と zodResolver にセットするだけで、先程 Pydantic で定義した型とバリデーションルールを再利用することができます。
import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { UserCreateRequest } from "@/lib/api/schema/api_schemas"; export const UserForm = ({ onSubmit, isLoading }) => { const { register, handleSubmit, formState: { errors }, } = useForm<UserCreateRequest>({ resolver: zodResolver(UserCreateRequest), defaultValues: { role: "user" }, }); return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register("name")} placeholder="名前" /> {errors.name && <p>{errors.name.message}</p>} <input {...register("email")} type="email" placeholder="メール" /> {errors.email && <p>{errors.email.message}</p>} <input {...register("age", { valueAsNumber: true })} type="number" /> {errors.age && <p>{errors.age.message}</p>} <button type="submit" disabled={isLoading}> {isLoading ? "作成中..." : "ユーザーを作成"} </button> </form> ); };
実際にサンプルコードのフロントエンドサーバーを起動して、フォームに不正な値を設定すると、以下のように期待通りバリデーションを実行してくれていることがわかります。

4. まとめ
本記事では、FastAPI、Pydantic、Zod、openapi-zod-client を組み合わせ、API の型とバリデーションルールを一元管理する方法を紹介しました。弊社では、この取り組みによって以下のメリットを得ることができました。
- 型の不一致によるバグを予防
- バックエンドで定義した OpenAPI スキーマからフロントエンドの型を生成するため、型の不一致によるバグを大幅に減少させることができました。
- バリデーションロジックを再利用
- Zod へ変換されたスキーマを React Hook Form などに直接適用できるため、同一のルールを重複して実装する必要がなくなりました。
- 変更への追従が容易
API の型・バリデーションロジックの管理に課題感を持っている方がいましたら、ぜひ導入を検討してみてください!

書いた人:大瀧
