チュートリアル

OpenAPI から JSON API ドキュメント、TypeScript 型、API Client を自動生成するには?Schema からコード生成まで

手書きドキュメント、手書き型、手書き fetch が別々に漂うコストは、エンドポイント自体より高い。OpenAPI Schema を契約にし、ドキュメントも型も Client も同じ JSON から伸ばす。

GET /invoices で高いのは、実装より隣の三つの賞味期限だ。人向け API ドキュメント、コンパイラ向け TypeScript 型、実行時の fetch 包み。今も Wiki に見本 JSON を貼り、types.ts に同じ欄を手で書き、api.ts で URL を繋ぐチームがある。OpenAPI Schema の価値は、これを機械が読める一つの契約に畳むことだ。あなたは openapi.json(または YAML)を保守する。ドキュメントサイト、型ファイル、Client はそこから生成される。欄を一度変えれば三箇所が動く。ツール入参は MCP と JSON Schema、モデルの最終答は 構造化出力。本稿は HTTP JSON API だけだ。Schema からドキュメント、型、Client。規範は OpenAPI 3.1。既定の生成器は openapi-typescript。

一份の Schema、三つの産物:ドキュメント、型、Client

目標を先に固定する。OpenAPI ドキュメントは別の Markdown ではない。同じ spec の描画だ。Scalar、Swagger UI、Redoc は読者に過ぎない。TypeScript 型は第二の手書き interface ではない。paths と components.schemas の投影だ。API Client は「もう一枚の axios 包み」ではない。operationId か経路から生まれ、要求と応答の型を帯びた関数だ。三つのうち二つが手書きなら、Schema は壁のポスターだ。

道具は後で選ぶ。契約が先だ。FastAPI、Nest、Spring、Go の注釈から spec を出しても、openapi.json を先に書いて実装を合わせてもよい。禁じるのは、先に Client を書き、呼び出し側の「感じ」でドキュメントを足すことだ。単一の真実がなく、CI はどちらが正しいか判断できない。Agent が REST を道具にするなら、この OpenAPI を読むべきだ。第二の MCP inputSchema を写してはならない。二つの契約はすぐに drift する。層は上の MCP 文を見よ。

OpenAPI 3.1 は components.schemas を JSON Schema 2020-12 に寄せる。可空は type: ["string", "null"] であり、3.0 の nullable: true ではない。「ドキュメントは任意、型は必須」が減る。JSONVue で見るオブジェクトは、生成器が食べるオブジェクトであるべきだ。書き出すときに方言へ変換してはならない。

産物 出所 手で書いてはいけないもの
JSON API ドキュメント同じ OpenAPI JSON を Scalar / Swagger UI で描くWiki の見本 JSON と期限切れ截図
TypeScript 型openapi-typescript が paths / components を出すspec と並行する interface Invoice
API Client / hooksopenapi-fetch、Orval、hey-api が呼び出し関数を出す手組み URL と as Invoice

先に OpenAPI JSON を正しく書く:paths、components、operationId

生成器の上限は spec の質だ。最低限は openapi 版、info、paths、再利用モデルを components.schemas へ。各操作に安定した operationId(listInvoices、createInvoice)を付ける。Client はそれを関数名にする。経路が変わっても id を漂わせるな。要求と成功応答は application/json を宣言し、コンポーネントへ $ref する。各 path に欄表を複製するな。

失敗応答も契約に入れる。400 / 401 / 404 / 422 に JSON Schema を付ければ、Client は unknown ではなく読める連合型にできる。クエリと経路パラメータは parameters に書け。body に隠し、ドキュメントで口約束するな。Bearer や OIDC は securitySchemes に書く。生成 Client は token の置き場を知る。認証プロトコル自体は OAuth 2.1。REST でも token はヘッダであり、JSON 封筒ではない。

下が最小の請求 API だ。製品 spec ではないが、ドキュメント、型、Client の三本を同時に回せる。Invoice は一度だけ現れ、list と create がそれを参照する。まずローカルで JSON 整形して parse を確認し、Schema で $ref が切れていないか見る。

{
  "openapi": "3.1.1",
  "info": {
    "title": "Invoices API",
    "version": "2026.09.18"
  },
  "paths": {
    "/invoices": {
      "get": {
        "operationId": "listInvoices",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["draft", "paid", "void"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Invoice list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["items"],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Invoice" }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createInvoice",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/InvoiceDraft" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Invoice" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "InvoiceDraft": {
        "type": "object",
        "required": ["customerId", "amount"],
        "properties": {
          "customerId": { "type": "string" },
          "amount": { "type": "integer", "minimum": 1 },
          "note": { "type": ["string", "null"] }
        }
      },
      "Invoice": {
        "allOf": [
          { "$ref": "#/components/schemas/InvoiceDraft" },
          {
            "type": "object",
            "required": ["id", "status"],
            "properties": {
              "id": { "type": "string" },
              "status": {
                "type": "string",
                "enum": ["draft", "paid", "void"]
              }
            }
          }
        ]
      }
    }
  }
}

人が読む JSON API ドキュメントを生成する

正しいドキュメントサイトは、構築時に openapi.json を静的ディレクトリへコピーし、隣に読者を置く。Scalar、Swagger UI、Redoc は同じファイルを読む。「ドキュメント」を別の Markdown の山だと思い、CI で spec と二度突き合わせてはならない。人がドキュメントを直し、機械が spec を直すと、翌日に分岐する。

読者が示せるのは spec に書いたものだけだ。example がなければ複製できる見本がない。失敗応答がなければ失敗の説明がない。description が空ならページに経路だけが残る。ドキュメントを生成する前に契約を埋めよ。プレビューはブラウザで読者を開けばよい。spec 自体は JSONVue で構造を見る方が、巨大 YAML で切れた $ref を探すより速い。

ドキュメント URL を info.contact や内部ポータルに書いてよい。ただし製品向けの第二の欄表を保守するな。製品が見たい列挙と必須は、すでに Schema の enum / required にある。二枚の欄表は drift の定番入口だ。

TypeScript 型を生成する:openapi-typescript

型生成の既定は openapi-typescript で足りる。実行時ゼロ、OpenAPI 3.0 と 3.1 を食べ、paths と components を出す。コマンドは npx openapi-typescript openapi.json -o src/api/schema.d.ts。遠隔 spec も使える。本番 CI はリポジトリ内のファイルを釘付けし、変わる URL を打ちに行かせるな。

import type { paths, components } from "./schema";

type Invoice = components["schemas"]["Invoice"];
type ListOk =
  paths["/invoices"]["get"]["responses"][200]["content"]["application/json"];

export type { Invoice, ListOk };

型は経路で索引する。新たに interface を宣言するな。paths["/invoices"]["get"]["responses"][200]["content"]["application/json"] が一覧の成功体だ。モデルを再利用するなら components["schemas"]["Invoice"]。経路が変われば、三つの Invoice を探す代わりに、呼び出し側でコンパイラが怒る。

型は実行時検証ではない。生成された .d.ts はブラウザに存在しない。回線は欠けた JSON をまだ運ぶ。コンパイル通過は、契約に沿って Client を書いたことだけを意味する。実行時の門が要るなら、Orval/Kubb で Zod を出すか、重要な応答を Schema 検証する。「TypeScript が緑」を唯一の検査にするな。

API Client を生成する:openapi-fetch、Orval、hey-api

型だけでよいなら openapi-fetch を添える。createClient<paths>({ baseUrl }) のあと client.GET("/invoices", { params: { query } })。本文、クエリ、経路パラメータは型で止まる。createInvoice の body を list に送れない。これが 2026 フロントの薄い既定だ。ファイルは少なく、fetch は自分で握り、生成物の倉庫はない。

import createClient from "openapi-fetch";
import type { paths } from "./schema";

const client = createClient({
  baseUrl: "https://api.example.com",
  headers: { Authorization: "Bearer …" }
});

const { data, error } = await client.GET("/invoices", {
  params: { query: { status: "paid" } }
});

const created = await client.POST("/invoices", {
  body: { customerId: "cus_1", amount: 1999, note: null }
});

React Query hooks、Zod、MSW mock が要るなら Orval を上げる。同じ spec から関数とフックが伸びる。すでに Query をデータ層にしている応用に合う。hey-api(@hey-api/openapi-ts)は SDK 関数とインターセプタだ。operationId をメソッド名にしたいが、数千ファイルは要らないチーム向け。OpenAPI Generator の typescript-axios はまだ動くが出力は重い。新規は第一選択にしなくてよい。

どの流水線でも、生成ファイルを手で直すな。振る舞いを変えるなら spec、生成器設定、または手書きの包み(タイムアウト、再試行、テナント頭)だ。生成物を手直しするのは契約を捨てることだ。token は Authorization ヘッダへ。JSON body に埋めるな。

道具 得られるもの 選ぶとき
openapi-typescript + openapi-fetch型 + 薄い fetch Client、実行時はほぼゼロ既定の起点。キャッシュを自分で握るとき
Orval型 + 関数 + Query hooks + 任意の Zod/MSWすでに TanStack Query を使い、mock を spec に従わせたい
hey-api / KubbSDK メソッド名、またはプラグイン多産物インターセプタ基盤、または operation ごとに一つのファイル

CI で drift を止め、JSONVue で契約を見る

契約が変われば生成物を再実行する。CI では spec を検証(Spectral または Redocly)→ 生成器を走らせ → 生成ディレクトリを git diff --exit-code。diff があれば失敗させ、新しい型を提出させる。ノートで一度 codegen して忘れるな。バックエンドが欄を変え、フロントの型が先週のまま、というのがこの事故の標準形だ。

npx --yes openapi-typescript openapi.json -o src/api/schema.d.ts
npx --yes @redocly/cli lint openapi.json
git diff --exit-code -- src/api/schema.d.ts

結合では少なくとも三つの JSON を扱う。OpenAPI spec 自体、ある 200 の body、ある 422 の失敗体。それに生成器へ渡す薄い夹具を足す。役割が違う。「汎用検証」一つのファイルに混ぜるな。失敗見本を一つずつ残す。切れた $ref、欠けた operationId、応答 content に application/json がない、列挙だけ変わった二版 spec。

ブラウザで終えられる:JSON 整形で spec が parse できるか見、JSON Schema 検証で components.schemas と実 body を確かめ、JSON Diffで二版 OpenAPI や「モデル arguments 対下流 body」を比べる。データはこの機械を出ない。続き:MCP と JSON Schema、構造化出力、同じ欄表からツール外壳を生成すること。

関連:MCP と JSON Schema、AI 構造化出力、AI が JSON を壊すとき、MCP とは。

よくある質問 FAQ

spec は YAML か JSON か?

生成器は両方を食べる。倉庫では YAML の方が人の diff に優しい。ブラウザと JSONVue には JSON を送れ。CI では源ファイルを一つに釘付けし、もう一方は構築産物だけにする。二つの源が互いを直すな。

型だけ生成し、Client は出さなくてよいか?

よい。むしろそうすべきことが多い。openapi-typescript は .d.ts だけを出す。既存の axios 包みは、これらの型を少しずつ食べ始められる。経路と body が型で囲まれてから openapi-fetch や Orval に替える。初日に三千ファイルを出すより制御しやすい。

新規は OpenAPI 3.0 か 3.1 か?

新しい契約は 3.1。JSON Schema 2020-12 に揃え、可空と const の方言を一層減らせる。既存 3.0 を無理に上げるな。生成器(openapi-typescript 7.x、Orval 8)が 3.1 を宣言してから迁せ。Swagger 2.0 は先に 3.x へ上げてから Client を語れ。

OpenAPI と MCP inputSchema を一份にできるか?

業務欄は一份の canonical JSON Schema を共有し、OpenAPI の requestBody と MCP の inputSchema を別々に生成できる。OpenAPI ファイルごとを MCP Client のツール一覧に渡すな。伝送、認証、メソッド名は同じ層ではない。一份の欄表、二枚の外壳の方が、複製より drift しにくい。

まとめと次の一歩

OpenAPI コード生成は一文で書ける。機械が読める Schema が、人が読む JSON API ドキュメント、コンパイル時 TypeScript 型、型付き Client を同時に伸ばす。どれか一份を手で書けば、契約は契約でなくなる。

載せる順:openapi.json が parse でき、operationId があり、失敗応答が揃う。次にドキュメント読者。それから型。最後に Client を選ぶ。spec、成功 body、失敗 body を JSONVue でこの機械に残す。ツール入参は MCP と JSON Schema。モデルの答は構造化出力。