💎

OrvalでスキーマとしてZodの選択と、Fetchクライアントでのランタイムバリデーションが可能になりました

に公開

はじめに

Orvalではv8.0.0より、Zodの使い方とFetchクライアントとの統合が更に強化されます。
v8.0.0はこの記事の執筆時点ではまだリリースされていませんがRCバージョンをインストールすることで試すことができます。

https://github.com/orval-labs/orval/releases/tag/v8.0.0-rc.5

本記事では、Zodスキーマ生成の設定簡素化と、FetchクライアントでのZodランタイムバリデーション機能について説明します。

具体的には、以下の2つの機能を実装しました。

  1. output.schemasオプションをオブジェクト形式に拡張し、生成タイプを選択可能に
  2. Fetchクライアントにランタイムバリデーション機能を追加

1. スキーマ生成タイプの選択機能

これまで

TypeScript型定義とZodスキーマを生成するには別々の設定が必要でした。

export default defineConfig({
  // TypeScript型定義用の設定
  petstore: {
    output: {
      schemas: 'src/gen/models',
      client: 'fetch',
    },
  },
  // Zodスキーマ用の設定
  petstoreZod: {
    output: {
      client: 'zod',
      fileExtension: '.zod.ts',
    },
  },
});

同じOpenAPI仕様に対して複数の設定を管理する必要があり設定の煩雑さが課題でした。

これから

schemasの生成タイプでzodを選択できるようになりました。

 export default defineConfig({
   petstore: {
     output: {
-      schemas: 'src/gen/models',
+      schemas: {
+        path: 'src/gen/models',
+        type: 'zod',
+      },
       client: 'fetch',
     },
   },
-  petstoreZod: {
-    output: {
-      client: 'zod',
-      fileExtension: '.zod.ts',
-    },
-  },
 });

TypeScriptの型定義とZodのスキーマ両方を使いたい場合、これまではそれぞれ別の出力を行う必要がありましたが、Zodスキーマだけで両方を兼ねることができるので余分なファイル出力を抑制することができます。

生成されるファイル

OpenAPI仕様の例:

components:
  schemas:
    Pet:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        tag:
          type: string

生成されるZodスキーマ pet.zod.ts:

/**
 * Generated by orval
 * Do not edit manually.
 */
import { z as zod } from 'zod';

export const Pet = zod.object({
  id: zod.number(),
  name: zod.string(),
  tag: zod.string().optional(),
});

export type Pet = zod.infer<typeof Pet>;

使い方

生成されたZodスキーマは、スキーマと型の両方として使用できます。
以下の例ではPetTypeScriptの型としての使用と同時にZodスキーマとしてparse()でバリデーションを行っています。

import { Pet } from './model/pet.zod';

const createPet = (data: unknown): Pet => {
  return Pet.parse(data);
};

2. Fetchクライアントのランタイムバリデーション

「1. スキーマ生成タイプの選択機能」によりZodスキーマとTypeScriptの型情報を同時に使えるようになったので、これをFetchクライアントのランタイムバリデーションとして実行できるようにしました。
例えば、外部APIの仕様変更、開発環境と本番環境の差異など、サーバーサイドのレスポンスがOpenAPI定義との一致を保障できないケースにてランタイムエラーを検知できるようになります。

設定方法

 export default defineConfig({
   petstore: {
     output: {
       schemas: {
         path: 'src/gen/models',
         type: 'zod',
       },
       client: 'fetch',
+      override: {
+        fetch: {
+          runtimeValidation: true,
+        },
+      },
     },
   },
 });

schemas.type'zod'に指定する必要があることに注意してください。

生成されるコード

+import { z as zod } from 'zod';
+import { Pet } from '../model/pet.zod';

 export const getPet = async (petId: number): Promise<Pet> => {
   const res = await fetch(`/pets/${petId}`, {
     method: 'GET',
   });

   const body = [204, 205, 304].includes(res.status) ? null : await res.text();
-  const data: Pet = body ? JSON.parse(body) : {};
+  const parsedBody = body ? JSON.parse(body) : {};
+  const data = Pet.parse(parsedBody);
   return data;
 };

runtimeValidationtrueにすることで、Zodスキーマをインポートし、Pet.parse()でレスポンスを検証するコードが生成されます。バリデーションエラーはZodErrorとして投げられます。

使用例

import { getPet } from './api/endpoints';
import { ZodError } from 'zod';

try {
  getPet(123);
} catch (error) {
  if (error instanceof ZodError) {
    error.errors.forEach((err) => {
      console.error(`  ${err.path.join('.')}: ${err.message}`);
    });
  }
}

関連PR

この機能は以下のIssue/PRで実装されました:

おわりに

これらの改善により、Orvalのスキーマ生成がより柔軟になりFetchクライアントのランタイム型安全性が大幅に向上しました。
バックエンドがOpenAPIのスキーマに準拠されていないケースでの活用や、インテグレーションテストでの活用など幅が広がると考えています。

なお、これらの機能改善はイベントでお会いした@karan_coronsさんと@r315drさんとの会話の中で着想を得たものです。貴重なフィードバックをいただきありがとうございました。

ぜひお試しください!

Discussion