# FlaskでOpenAPI/Swaggerを自動生成する：Flask-smorestでスキーマ駆動のREST APIとAPIドキュメントを本番品質で作る

> Flask-smorest 0.47でOpenAPI/Swaggerを自動生成する実装ガイド。Flask+marshmallow+webargs+apispecを束ね、1つのスキーマで入力検証・レスポンス整形・OpenAPI仕様を同時生成する。@blp.arguments/@blp.response、Swagger UI/ReDoc、ページネーション、エラー文書化、本番でのSwagger UI保護、CIでのopenapi.json生成までを実コードで解説します。

- 公開日: 2026-06-26
- 著者: 友田 陽大
- タグ: Python, Flask, OpenAPI, Swagger, REST API, marshmallow, アーキテクチャ設計
- URL: https://tomodahinata.com/blog/flask-openapi-swagger-flask-smorest-api-documentation-guide

## 要点

- 手書きFlask APIには機械可読な契約がない。FastAPIはOpenAPI/Swaggerを無料で得るが、Flask-smorestはWSGI/Flaskのまま同じ価値を手に入れる
- Flask-smorestはFlask+marshmallow+webargs+apispecの束。1つのmarshmallowスキーマが入力検証・レスポンス整形・OpenAPI仕様を同時に駆動する（契約の単一情報源）
- @blp.arguments(Schema)が検証済み引数を注入し、@blp.response(code, Schema)が戻り値をシリアライズしステータスを設定。422エラーも自動で文書化される
- @blp.paginate()でページネーション（X-Paginationヘッダ）、abort()で文書化された一貫エラー、url_prefixでバージョニングを表現する
- 本番ではSwagger UIを認証ゲート/無効化で守り、CIでopenapi.jsonを静的生成してクライアントのコード生成・型安全に繋ぐ。経済産業大臣賞B2B SaaSのFlask REST API知見を根拠に解説

---

## **導入：あなたのFlask APIには「契約」がない**

手で書いた Flask の REST API を、別チーム（フロントエンド、モバイル、外部パートナー）に渡す場面を思い浮かべてください。彼らが最初に聞くのは決まってこうです。「このエンドポイントのリクエストボディは何？レスポンスはどんな形？エラーのときは何が返る？」

そして、その答えはどこにあるでしょうか。多くの Flask 案件では、答えは **ビュー関数のコードを読むか、Slack で聞くか、Postman のコレクションを共有するか**——いずれも、コードとは別に手で維持される「実体のないドキュメント」です。コードが変わってもドキュメントは変わらない。やがて両者は乖離し、ドキュメントは「嘘をつくドキュメント」になります。これは技術的負債そのものです。

問題の本質は、**手書きの Flask API には機械可読な契約（contract）がない**ことです。[Flask の REST API 設計（MethodView / Blueprint / バージョニング）](/blog/flask-rest-api-design-methodview-blueprint-versioning-guide)を丁寧に行っても、その設計は人間が読むコードの中にしか存在せず、API の利用者が機械的に参照できる仕様（OpenAPI ドキュメント）にはなりません。

ここで多くの人が思い出すのが FastAPI です。[Flask vs FastAPI vs Django の比較](/blog/flask-vs-fastapi-vs-django-comparison-guide)でも触れたとおり、FastAPI は型ヒントから **OpenAPI 仕様と Swagger UI を「無料で」生成**します。これは FastAPI の決定的な魅力であり、Flask が「持っていない」と見なされる代表例です。

本記事の主張はシンプルです。**その差は、Flask 側で `Flask-smorest` を使えば埋まる**。WSGI / Flask のまま、1 つの marshmallow スキーマから OpenAPI 仕様・Swagger UI・ReDoc を自動生成し、入力検証とレスポンス整形まで同じスキーマで担う——FastAPI が型で得るものを、Flask は marshmallow スキーマで得るのです。

筆者は、経済産業大臣賞を受賞した B2B SaaS のバックエンドを **Python / Flask / SQLAlchemy / PostgreSQL** で設計・実装し、本番運用してきました。社内のフロントエンドチーム、そして API を叩く外部パートナーに対して「機械可読な契約」を提供することが、開発速度と信頼の両方を支えました。本記事は、その実戦で必要だった「ドキュメント自動化」の設計を、Flask-smorest 公式ドキュメントに忠実な実コードで体系化します。

> 💡 **この記事で扱うバージョン**：**Flask-smorest 0.47.0** を前提とします。Flask-smorest は「Flask / Marshmallow ベースの REST API フレームワーク」で、依存は **flask>=3.0.2,<4 / marshmallow>=3.24.1,<5 / webargs>=8 / apispec[marshmallow]>=6**、**Python 3.10 以上**が必要です。本稿のコードは公式ドキュメントのパターンに基づきます。marshmallow スキーマそのものの設計は [marshmallow 実践ガイド](/blog/marshmallow-python-serialization-validation-production-guide) を前提知識とします。

---

## **1. Flask-smorest とは何か：4 つのライブラリを束ねた「スキーマ駆動の核」**

Flask-smorest は、ゼロから API フレームワークを作り直したものではありません。**既に成熟した 4 つのライブラリを束ね、それらを「1 つのスキーマで駆動する」ための薄い接着層**です。

| 構成要素 | 役割 | smorest における位置づけ |
|---|---|---|
| **Flask** | WSGI アプリケーション・ルーティング | 土台。`Api(app)` で拡張として載せる |
| **marshmallow** | スキーマによる検証・シリアライズ | **契約の単一情報源**（入力も出力も仕様も） |
| **webargs** | リクエストのパース（query/json/path…） | `@blp.arguments` の中身 |
| **apispec** | marshmallow スキーマ → OpenAPI 仕様の変換 | ドキュメント自動生成のエンジン |

この構成が意味するのは、**1 つの marshmallow スキーマが、3 つの仕事を同時にこなす**ということです。

1. **入力境界の検証**（webargs 経由で `load`）：クライアントから来た JSON / query を検証し、不正なら自動で 422 を返す
2. **出力境界の整形**（`dump`）：戻り値をスキーマで整形し、内部属性の漏洩を防ぐ
3. **OpenAPI 仕様の生成**（apispec 経由）：同じスキーマから request body / response の JSON Schema を生成し、Swagger UI に表示する

[marshmallow × Flask × SQLAlchemy のガイド](/blog/marshmallow-flask-sqlalchemy-rest-api-production-guide)では、「1 つのスキーマが入口と出口の 2 つの境界を守る」ことを論じました。Flask-smorest はそこに **3 つ目の境界——機械可読なドキュメント——**を、追加コストゼロで足します。これが DRY の極致です。スキーマを書き換えれば、検証・整形・ドキュメントが**同時に**追従するので、乖離する隙間がそもそも存在しません。

> 💡 **発想の転換**：FastAPI は「Pydantic の型ヒント」を単一情報源にして OpenAPI を生成します。Flask-smorest は「marshmallow スキーマ」を単一情報源にして同じことをします。**手段（型ヒント vs スキーマオブジェクト）が違うだけで、得られる価値は同型**です。すでに marshmallow に投資している Flask プロジェクトなら、ASGI へ移行せずに、その投資を OpenAPI ドキュメントへ転用できます。

---

## **2. クイックスタートを精読する：4 つの新しい概念**

公式クイックスタートは短いですが、ここには Flask-smorest を理解するための要素が凝縮されています。まず全体を見てから、1 行ずつ解剖します。

```python
from flask import Flask
from flask.views import MethodView
import marshmallow as ma
from flask_smorest import Api, Blueprint, abort
from .model import Pet

app = Flask(__name__)
app.config["API_TITLE"] = "My API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.2"
api = Api(app)


class PetSchema(ma.Schema):
    id = ma.fields.Int(dump_only=True)
    name = ma.fields.String()


class PetQueryArgsSchema(ma.Schema):
    name = ma.fields.String()


blp = Blueprint("pets", "pets", url_prefix="/pets", description="Operations on pets")


@blp.route("/")
class Pets(MethodView):
    @blp.arguments(PetQueryArgsSchema, location="query")
    @blp.response(200, PetSchema(many=True))
    def get(self, args):
        """List pets"""
        return Pet.get(filters=args)

    @blp.arguments(PetSchema)
    @blp.response(201, PetSchema)
    def post(self, new_data):
        """Add a new pet"""
        item = Pet.create(**new_data)
        return item


api.register_blueprint(blp)
```

ここに、Flask-smorest 特有の概念が 4 つ登場します。順に見ます。

### **2.1 `Api(app)`：拡張として載せる**

`api = Api(app)` で、Flask-smorest を拡張としてアプリに装着します。この `Api` オブジェクトが、apispec を内部に抱え、登録された Blueprint を走査して OpenAPI 仕様を組み立て、Swagger UI / ReDoc のエンドポイントを配信する司令塔です。アプリケーションファクトリと併用するなら、[Flask 本番運用ガイド](/blog/flask-production-guide)の `init_app` パターンに合わせて `api.init_app(app)` を使えます。

> ⚠️ `API_TITLE` / `API_VERSION` / `OPENAPI_VERSION` の 3 つは **必須**です。これらが無いと `Api(app)` が起動時に失敗します。`API_VERSION`（あなたの API のバージョン、例 `v1`）と `OPENAPI_VERSION`（OpenAPI 仕様自体のバージョン、例 `3.0.2`）は別物なので混同しないでください。

### **2.2 smorest の `Blueprint`：Flask 標準の Blueprint ではない**

最初の落とし穴がここです。

```python
from flask_smorest import Api, Blueprint, abort
```

この `Blueprint` は **`flask.Blueprint` ではなく、Flask-smorest 独自の Blueprint** です。Flask 標準の Blueprint を継承し、OpenAPI を理解する装飾子（`@blp.arguments` / `@blp.response` / `@blp.paginate` …）を追加で備えています。コンストラクタは `description=` を受け取り、これが OpenAPI のタグ説明になります。

同様に `abort` も `flask.abort` ではなく **smorest の強化版 `abort`** です。エラーメッセージや追加情報を JSON エラーレスポンスに乗せられます（§5 で詳説）。

> ⚠️ **アンチパターン**：`from flask import Blueprint, abort` と `from flask_smorest import Blueprint, abort` を混在させる。前者を使うと `@blp.arguments` が存在せず `AttributeError` になります。Flask-smorest を使うファイルでは、Blueprint / abort は **必ず `flask_smorest` から** import してください。

### **2.3 `@blp.route("/")` はクラスを装飾する**

```python
@blp.route("/")
class Pets(MethodView):
    def get(self, args): ...
    def post(self, new_data): ...
```

Flask-smorest は **クラスベースビュー（`MethodView`）を第一級**で扱います。`@blp.route("/")` が**クラス全体**を装飾し、クラス内の `get` / `post` メソッドが、それぞれ HTTP の GET / POST にマップされます。`MethodView` は Flask コアのクラス（`flask.views.MethodView`）で、smorest 独自ではありません。

1 つの URL に対する複数の HTTP メソッドを 1 つのクラスにまとめられるので、リソース指向の REST 設計と自然に噛み合います。`MethodView` 自体の設計思想（リソース = クラス、メソッド = HTTP 動詞）は [Flask REST API 設計ガイド](/blog/flask-rest-api-design-methodview-blueprint-versioning-guide)で詳しく扱っています。本記事はその設計に「ドキュメント自動化」を重ねます。

### **2.4 装飾子の順序とドキュストリング**

メソッドに付く 2 つの装飾子と、その下のドキュストリングに注目してください。

```python
@blp.arguments(PetSchema)          # 入力：検証して引数に注入
@blp.response(201, PetSchema)      # 出力：シリアライズしてステータス設定
def post(self, new_data):
    """Add a new pet"""            # ← OpenAPI の summary になる
    item = Pet.create(**new_data)
    return item
```

- `@blp.arguments(PetSchema)`：リクエストボディを `PetSchema` で検証し、検証済みの dict を `new_data` 引数として注入する（§3）
- `@blp.response(201, PetSchema)`：戻り値 `item` を `PetSchema` でシリアライズし、HTTP 201 を設定する（§4）
- `"""Add a new pet"""`：**ドキュストリングが OpenAPI の operation summary** になる。Swagger UI に表示される説明文の源泉

この 3 つが揃うことで、`POST /pets` は「リクエストボディは PetSchema、成功時は 201 で PetSchema を返す、説明は Add a new pet」という機械可読な仕様として、自動的にドキュメントへ載ります。**ビューを書くこと自体がドキュメントを書くこと**になっているのが、Flask-smorest の核心です。

---

## **3. `@blp.arguments`：入力境界の検証と注入**

`@blp.arguments` は、Flask-smorest の入口側の主役です。やることは 2 つ。

1. 指定された **location** からリクエストデータを取り出し、スキーマで**検証（`load`）**する
2. 検証済みのデータを、ビュー関数に**引数として注入**する

検証に失敗すれば、Flask-smorest が自動で **422 Unprocessable Entity** を返します。ビュー関数の中で `try/except ValidationError` を書く必要はありません——境界の検証が宣言的に外出しされます。

### **3.1 location：どこから読むか**

`@blp.arguments(Schema, location=...)` の `location` で、データの取得元を指定します。

| location | 取得元 | 用途 |
|---|---|---|
| `json`（既定） | リクエストボディの JSON | POST / PUT のペイロード |
| `query` | クエリ文字列 | 一覧の絞り込み・検索パラメータ |
| `path` | URL パスパラメータ | リソース ID |
| `form` | フォームデータ | HTML フォーム送信 |
| `headers` | リクエストヘッダ | カスタムヘッダの検証 |
| `cookies` | Cookie | — |
| `files` | アップロードファイル | マルチパート |
| `json_or_form` | JSON またはフォーム | 両対応エンドポイント |

`location` を省略すると `json` が既定です。クエリパラメータを検証したいときは、クイックスタートの GET のように `location="query"` を明示します。

### **3.2 注入のされ方：位置引数 / キーワード引数 / スタッキング**

既定では、`@blp.arguments` は検証済みデータを **1 つの位置引数（dict）** として注入します。

```python
@blp.arguments(PetSchema)
def post(self, new_data):   # new_data は検証済み dict
    ...
```

`as_kwargs=True` を渡すと、dict ではなく **`**kwargs`** として展開注入されます。

```python
@blp.arguments(PetSchema, as_kwargs=True)
def post(self, name, **kwargs):   # スキーマのフィールドが個別のキーワード引数に
    ...
```

複数の `@blp.arguments` を **スタッキング**すると、宣言順に複数の位置引数が注入されます。query と json を同時に検証したい場合に有効です。

```python
@blp.arguments(PetQueryArgsSchema, location="query")
@blp.arguments(PetSchema, location="json")
def post(self, query_args, body):
    # query_args = query から、body = JSON から（装飾子の順に対応）
    ...
```

> 💡 スタッキングの引数順は「上から下」の装飾子順に対応します。可読性のため、location が異なる場合は変数名を `query_args` / `body` のように **取得元が分かる名前**にしておくと、後から読む人が迷いません。

### **3.3 422 が「自動で文書化される」**

ここが手書き API との決定的な違いです。`@blp.arguments` を付けたエンドポイントには、**検証エラー（422）のレスポンスが OpenAPI 仕様に自動で追加**されます。つまり、API の利用者は Swagger UI を見るだけで「不正な入力を送ると 422 が返り、エラーの形はこうだ」と機械的に知ることができます。手書きなら、この 422 の存在と形は「コードを読まないと分からない暗黙知」でした。Flask-smorest はそれを明示の契約に変えます。

---

## **4. `@blp.response`：出力境界の整形とステータス設定**

`@blp.response(status_code, Schema)` は出口側の主役です。やることは 3 つ。

1. ビュー関数の**戻り値をスキーマで `dump`**（シリアライズ）する
2. HTTP の**ステータスコードを設定**する
3. そのレスポンス（ステータス + スキーマ）を **OpenAPI 仕様に登録**する

```python
@blp.response(200, PetSchema(many=True))
def get(self, args):
    return Pet.get(filters=args)   # ORMオブジェクトのリスト → PetSchema で整形
```

リストを返すときは `Schema(many=True)` を使います。`many=True` は OpenAPI 仕様にも反映され、「レスポンスは配列だ」と正しく文書化されます。

`dump_only` フィールド（例 `id = ma.fields.Int(dump_only=True)`）の威力もここで効きます。`id` は入力（`@blp.arguments`）では受け付けず（マスアサインメント防止）、出力（`@blp.response`）では返す——1 つのスキーマで「読み取り専用属性」を宣言できます。この入出力非対称の設計は [marshmallow の serialization/validation ガイド](/blog/marshmallow-python-serialization-validation-production-guide)で詳述したとおりで、smorest はそれをそのまま OpenAPI の `readOnly` に翻訳します。

> ⚠️ **重要な例外（公式の注意）**：ビュー関数が `werkzeug.BaseResponse`（= `Response` オブジェクトや `make_response()` の結果）を返した場合、**その Response はそのまま返され、スキーマによる `dump` もステータスコードの適用も行われません**。ファイルダウンロードやリダイレクトのように Response を直接組み立てるケースでは、`@blp.response` のスキーマ整形は効かないと理解してください。スキーマで整形させたいなら、**dict や ORM オブジェクトを返す**（Response を作らない）のが鉄則です。

---

## **5. ページネーションとエラー：一覧と異常系を「文書化された契約」にする**

実務の REST API で最も「仕様の乖離」が起きやすいのが、**一覧（ページネーション）と異常系（エラー）**です。Flask-smorest は両方を契約として固定する仕組みを持っています。

### **5.1 `@blp.paginate()`：ページネーションを宣言する**

```python
@blp.route("/")
class Pets(MethodView):
    @blp.response(200, PetSchema(many=True))
    @blp.paginate()
    def get(self, pagination_parameters):
        pagination_parameters.item_count = Pet.size
        return Pet.get_elements(
            first_item=pagination_parameters.first_item,
            last_item=pagination_parameters.last_item,
        )
```

`@blp.paginate()` は `PaginationParameters` オブジェクトをビューに注入します。ここから得られるものと、あなたがやるべきことは次のとおりです。

- **注入される**：`pagination_parameters`（`.page` / `.page_size`、計算済みの `.first_item` / `.last_item`）
- **あなたが設定する**：`pagination_parameters.item_count = <総件数>`（総ページ数の計算に必要）
- **自動で付く**：ページネーションのメタデータが **`X-Pagination` レスポンスヘッダ**に載る

既定のパラメータは `DEFAULT_PAGINATION_PARAMETERS = {"page": 1, "page_size": 10, "max_page_size": 100}` です。`page` / `page_size` のクエリパラメータと、その既定値・上限（`max_page_size`）も OpenAPI 仕様に自動で文書化されます。クライアントは Swagger UI を見れば「`?page=2&page_size=50` で叩け、上限は 100 件だ」と機械的に分かります。

> 💡 ページネーション情報を**ボディ**ではなく **`X-Pagination` ヘッダ**に載せるのは設計判断です。レスポンスボディは「リソースの配列」に純粋化され、メタデータ（総件数・次ページ有無）はヘッダに分離されます。クライアントがメタデータをパースしやすく、ボディのスキーマがメタで汚れません。

### **5.2 `abort()`：文書化された一貫エラー**

異常系は、smorest の `abort` で返します。

```python
from flask_smorest import abort


@blp.route("/<int:pet_id>")
class PetById(MethodView):
    @blp.response(200, PetSchema)
    def get(self, pet_id):
        pet = Pet.get_by_id(pet_id)
        if pet is None:
            abort(404, message="Pet not found")
        return pet
```

smorest の `abort` は `flask.abort` を拡張し、**`message` などの追加情報を JSON エラーレスポンスに含められます**。Flask-smorest はエラーレスポンスを一貫した形（`code` / `status` / `message` / 検証エラー時は `errors`）に整え、しかもその形を OpenAPI 仕様に登録します。つまり **「このエンドポイントは 404 を返しうる、エラーの形はこうだ」が契約に載る**。

この「文書化された一貫エラーエンベロープ」は、手書きでやると必ず散らかります。[Flask のエラー処理・可観測性ガイド](/blog/flask-error-handling-logging-observability-guide)で論じた「全エンドポイントで統一された JSON エラー」を、smorest は標準で、しかもドキュメント化込みで提供します。

| 異常系 | 手書き Flask | Flask-smorest |
|---|---|---|
| 検証エラー（422） | `try/except` を各所に手書き | `@blp.arguments` が自動で 422 + 文書化 |
| Not Found（404） | `jsonify(...), 404` を散在 | `abort(404, message=...)` で一貫 + 文書化 |
| エラーの形 | エンドポイントごとにバラバラ | 統一エンベロープ + OpenAPI に登録 |
| 利用者への伝達 | コードを読む / 口頭 | Swagger UI で機械可読 |

---

## **6. OpenAPI 設定と UI 配信：Swagger UI / ReDoc を有効化する**

ここまでで「仕様が自動生成される」ことは分かりました。次は、その仕様を**どこで・どう配信するか**です。Flask-smorest は OpenAPI の JSON、Swagger UI、ReDoc を、設定だけで配信できます。

```python
# OpenAPI 配信の設定（公式の既定値つき）
OPENAPI_VERSION = "3.0.2"                       # 必須
OPENAPI_URL_PREFIX = "/"                        # 既定 None → 設定しないと仕様を配信しない
OPENAPI_JSON_PATH = "openapi.json"              # 既定。OpenAPI JSON の配信パス
OPENAPI_SWAGGER_UI_PATH = "/swagger-ui"         # 既定 None → 設定すると Swagger UI 有効
OPENAPI_SWAGGER_UI_URL = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
OPENAPI_REDOC_PATH = "/redoc"                   # 既定 None → 設定すると ReDoc 有効
OPENAPI_REDOC_URL = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"
```

押さえるべき既定値の挙動は 3 つです。

1. **`OPENAPI_URL_PREFIX` の既定は `None`**。これを設定しない限り、**OpenAPI 仕様自体が配信されません**。`"/"` などに設定して初めて、`/openapi.json` が見えるようになります。
2. **`OPENAPI_SWAGGER_UI_PATH` の既定も `None`**。設定して初めて Swagger UI が有効になります。上記の例なら `/swagger-ui` でインタラクティブな UI が見られます。
3. **`OPENAPI_REDOC_PATH` の既定も `None`**。設定して初めて ReDoc が有効になります。ReDoc は閲覧専用で読みやすい三カラムのドキュメントです。

> 💡 **Swagger UI と ReDoc の使い分け**：Swagger UI は「**叩ける**」（Try it out でブラウザから実リクエストを送れる）ので、開発・検証フェーズで強力です。ReDoc は「**読める**」（美しい静的ドキュメント）ので、外部公開・リファレンス用途に向きます。両方を有効化し、用途で使い分けるのが定石です。UI の JS 資産は CDN（jsDelivr）から読み込む構成が既定です。

---

## **7. 本番運用の作法：保護・バージョニング・CI でのコード生成**

「動く」と「本番品質」の間には溝があります。Flask-smorest を本番に出すときに、筆者が必ず詰める論点を挙げます。

### **7.1 Swagger UI を本番でどう扱うか**

開発で便利な Swagger UI は、**本番ではそのまま晒すべきではない**場合があります。理由は 2 つ。(a) 内部 API のエンドポイント・スキーマ・パラメータを全公開すると攻撃面の地図を渡すことになる、(b) 「Try it out」で本番データに対して破壊的操作を試せてしまう。対策は段階的に選べます。

| 戦略 | 方法 | 向くケース |
|---|---|---|
| **完全無効化** | 本番設定で `OPENAPI_SWAGGER_UI_PATH` を設定しない（`None`） | 完全な内部 API、UI 不要 |
| **認証ゲート** | UI 配信パスをリバースプロキシ / 拡張で Basic 認証や IP 制限の背後に置く | 社内のみ閲覧可にしたい |
| **JSON のみ配信** | `openapi.json` は出すが UI は出さない | 利用者は自前ツールで読む |
| **全公開** | UI も含め公開 | 公開 API・パートナー向けポータル |

設定が**環境ごとに切り替わる**点が肝です。[Flask 本番運用ガイド](/blog/flask-production-guide)で扱った「設定の 12-factor 化」に従い、`OPENAPI_SWAGGER_UI_PATH` を環境変数 / インスタンス設定から注入し、本番では無効・開発では有効、のように切り替えます。コードに固定値で書かないこと。

> ⚠️ 「Swagger UI を本番に出す = 即セキュリティ事故」ではありません。**認証必須の API なら、Try it out も認証が要る**ので、無条件に危険とは限りません。とはいえ「攻撃面の地図を無料で配る」コストは常にあるので、**公開 API でない限りは認証ゲートか無効化を既定**にするのが安全側の判断です。

### **7.2 バージョニング：`url_prefix` か 複数 `Api` か**

API のバージョニングは、smorest の Blueprint の `url_prefix` で素直に表現できます。

```python
v1 = Blueprint("orders_v1", "orders_v1", url_prefix="/api/v1/orders", description="Orders API v1")
v2 = Blueprint("orders_v2", "orders_v2", url_prefix="/api/v2/orders", description="Orders API v2")

api.register_blueprint(v1)
api.register_blueprint(v2)
```

同一の `Api` に複数バージョンの Blueprint を登録すれば、1 つの OpenAPI 仕様に v1 / v2 が並びます。バージョンごとに**仕様を完全に分離**したいなら、`Api` インスタンス自体を複数立て、それぞれ別の `OPENAPI_URL_PREFIX` で配信する構成も取れます。URL パスでのバージョニングの設計判断（パス vs ヘッダ vs メディアタイプ）は [Flask REST API 設計ガイド](/blog/flask-rest-api-design-methodview-blueprint-versioning-guide)に譲ります。

### **7.3 CI で `openapi.json` を静的生成し、クライアント型安全に繋ぐ**

ここが、Flask-smorest の投資が**最も大きく報われる**ポイントです。OpenAPI 仕様は、ただ「人が読むドキュメント」で終わらせてはもったいない。**機械が読んで、型付きのクライアントを自動生成する**ための入力にできます。

CI で OpenAPI 仕様を静的ファイルに書き出します。Flask-smorest の `Api` から仕様を取得できます。

```python
# scripts/dump_openapi.py — CIで実行し openapi.json を成果物にする
import json

from myapp import create_app


def main() -> None:
    app = create_app()
    api = app.extensions["flask-smorest"]["apis"][""]["ext_obj"]
    spec = api.spec.to_dict()
    with open("openapi.json", "w", encoding="utf-8") as f:
        json.dump(spec, f, ensure_ascii=False, indent=2, sort_keys=True)


if __name__ == "__main__":
    main()
```

```bash
# CI（GitHub Actions 等）でのフロー例
python scripts/dump_openapi.py            # サーバから仕様を抽出
npx @openapitools/openapi-generator-cli generate \
  -i openapi.json -g typescript-fetch -o ./generated-client
# 生成された型付きクライアントをフロント / モバイルが import
```

このフローが完成すると、**バックエンドのスキーマ変更 → CI で openapi.json 更新 → クライアント型の再生成 → コンパイルエラーで破壊的変更が即座に検出**、という型安全のパイプラインが成立します。サーバとクライアントの契約が、人間の注意力ではなく**型システムで保証**される。この「OpenAPI を中心に据えた端から端までの型安全」の全体像は [Next.js × Go の end-to-end 型安全ガイド](/blog/nextjs-go-openapi-end-to-end-type-safety)で詳しく論じています。バックエンドが Flask でも、OpenAPI を経由すれば同じ型安全の恩恵を受けられます。

> 💡 **tags / operationId の衛生**：自動生成されたクライアントのメソッド名は OpenAPI の `operationId` 由来になります。各 Blueprint に意味のある名前・`description` を与え、エンドポイントごとに明確なドキュストリングを書いておくと、生成されるクライアントのコードが読みやすくなります。「ドキュメントの質 = クライアントコードの質」だと意識してください。

---

## **8. 技術選定：smorest / APIFlask / 手書き / FastAPI の正直な比較**

Flask-smorest が唯一の正解ではありません。OpenAPI ドキュメントを得る手段は複数あり、それぞれに適所があります。[技術選定は「優劣」ではなく「適合」の問題](/blog/flask-vs-fastapi-vs-django-comparison-guide)だという原則に従い、正直な比較を示します。

### **8.1 APIFlask という選択肢**

`Flask-smorest` の有力な代替が **APIFlask 3.1.1** です。「Flask ベースの軽量 Web API フレームワーク」で、smorest と同様に OpenAPI 仕様・Swagger UI・ReDoc を自動生成します。最大の違いは、**スキーマアダプタが差し替え可能で、marshmallow スキーマと Pydantic モデルの両方を扱える**点です（Pydantic 対応は 3.x 系で入った比較的新しい機能です）。

```text
Flask-smorest : marshmallow 一択（marshmallow に最適化）
APIFlask      : marshmallow / Pydantic を選べる（pluggable schema adapter）
```

すでに Pydantic に投資している、あるいは将来 FastAPI への移行を見据えてスキーマ資産を Pydantic で持ちたい——そういうケースでは APIFlask の Pydantic 対応が効きます。逆に、すでに marshmallow で境界設計を固めている（本クラスタの読者の多くがそうでしょう）なら、marshmallow ネイティブの smorest が素直です。

### **8.2 決定表：いつ何を選ぶか**

| 選択肢 | OpenAPI 自動生成 | スキーマ | ランタイム | 選ぶべき場面 |
|---|---|---|---|---|
| **手書き Flask** | なし（自分で書く） | marshmallow 等を手で適用 | WSGI | 1〜2 エンドポイントの極小 API。ドキュメント不要 |
| **Flask-smorest** | **あり**（自動） | marshmallow | WSGI | 既存 Flask + marshmallow 資産を活かしつつ契約が欲しい |
| **APIFlask** | **あり**（自動） | marshmallow **or Pydantic** | WSGI | Flask のまま Pydantic を使いたい / 移行を見据える |
| **FastAPI** | **あり**（標準） | Pydantic（型ヒント） | ASGI | 新規・高並行 IO・型ヒント駆動・async をフルに使いたい |

意思決定はこう整理できます。

- **ASGI へ行ける新規プロジェクトで、型ヒント駆動・async を最大化したい → FastAPI**。OpenAPI が標準で付いてくる。
- **既存の Flask（WSGI）から動けない、または動きたくない → Flask-smorest / APIFlask**。WSGI のまま OpenAPI を得る。
- **その中で marshmallow 資産があるなら smorest、Pydantic で書きたいなら APIFlask**。
- **そもそも契約が要らない極小内部 API → 手書き**。ただし「契約が要らない」は時間とともに崩れる前提に注意。エンドポイントが 3 つを超え、利用者が別チームになった瞬間、smorest を入れる価値が立ちます。

> 💡 **「Flask だから OpenAPI は無理」は誤解**。FastAPI の OpenAPI 自動生成は確かに強力ですが、それは「FastAPI でしか得られない」ものではありません。Flask-smorest / APIFlask が、WSGI のまま同等の価値を提供します。フレームワーク移行（WSGI → ASGI）のコストと、ドキュメント自動化の価値を、**別々の天秤**で測ってください。「OpenAPI が欲しいから FastAPI に移行する」は、多くの場合オーバーキルです。

---

## **9. 実例：文書化された `/api/v1/orders` リソース**

理論を、B2B SaaS で実際に必要になる形に落とします。**受注（Order）の一覧と作成**を、検証・整形・ページネーション・エラー・ドキュメントまで揃った 1 つのリソースとして組みます。

### **9.1 スキーマ：契約の単一情報源**

```python
# schemas.py
import marshmallow as ma


class OrderSchema(ma.Schema):
    """受注リソース。dump_only で読み取り専用、required で必須を宣言する。"""

    id = ma.fields.Int(dump_only=True)
    order_number = ma.fields.String(dump_only=True)
    customer_id = ma.fields.Int(required=True)
    amount = ma.fields.Decimal(required=True, as_string=True, validate=ma.validate.Range(min=0))
    status = ma.fields.String(
        dump_only=True,
        validate=ma.validate.OneOf(["pending", "confirmed", "shipped", "cancelled"]),
    )
    created_at = ma.fields.DateTime(dump_only=True)


class OrderQueryArgsSchema(ma.Schema):
    """一覧の絞り込み条件。query から読む。"""

    customer_id = ma.fields.Int()
    status = ma.fields.String(validate=ma.validate.OneOf(["pending", "confirmed", "shipped", "cancelled"]))
```

`id` / `order_number` / `status` / `created_at` は **`dump_only`**——サーバが採番・管理する読み取り専用属性で、クライアントは入力できません（マスアサインメント防止）。`customer_id` / `amount` は **`required`** で、欠けていれば自動 422。この 1 つのスキーマが、入力検証・出力整形・OpenAPI 仕様を同時に駆動します。

### **9.2 リソース：ビュー = ドキュメント**

```python
# views.py
from flask.views import MethodView
from flask_smorest import Blueprint, abort

from .schemas import OrderSchema, OrderQueryArgsSchema
from .service import OrderService

blp = Blueprint(
    "orders",
    "orders",
    url_prefix="/api/v1/orders",
    description="受注の一覧取得・作成を行う API",
)


@blp.route("/")
class Orders(MethodView):
    @blp.arguments(OrderQueryArgsSchema, location="query")
    @blp.response(200, OrderSchema(many=True))
    @blp.paginate()
    def get(self, filters, pagination_parameters):
        """受注を一覧する（顧客・ステータスで絞り込み可、ページネーション対応）"""
        total, items = OrderService.list(
            filters=filters,
            first_item=pagination_parameters.first_item,
            last_item=pagination_parameters.last_item,
        )
        pagination_parameters.item_count = total
        return items

    @blp.arguments(OrderSchema)
    @blp.response(201, OrderSchema)
    def post(self, new_order):
        """受注を作成する"""
        if not OrderService.customer_exists(new_order["customer_id"]):
            abort(422, message="customer_id が存在しません")
        return OrderService.create(new_order)
```

このコードが生成する契約を読み取ってください。

- `GET /api/v1/orders`：query で `customer_id` / `status` で絞れる、`page` / `page_size` でページングできる（既定 10 件・上限 100 件）、200 で Order の配列を返す、総件数は `X-Pagination` ヘッダ
- `POST /api/v1/orders`：ボディは OrderSchema（`customer_id` / `amount` 必須、`id` 等は受け付けない）、成功時 201 で Order を返す、検証失敗で 422、業務エラーで 422（message つき）

**これらすべてが、Swagger UI に自動で載ります**。フロントチームは UI で「Try it out」して受注作成を試せ、外部パートナーは ReDoc で仕様を読み、CI は openapi.json から型付きクライアントを生成する。筆者の B2B SaaS では、この「叩ける契約」を提供できることが、社内のフロント実装・パートナー連携・営業の技術説明のすべてを加速しました。**ドキュメントを別途書く工数がゼロ**——スキーマとビューを書けば、それがそのまま契約になるからです。

> 💡 Service 層（`OrderService`）にビジネスロジックを逃がし、ビューは「HTTP ↔ スキーマ ↔ サービス」の薄い変換に徹している点に注目してください。これは [marshmallow × Flask × SQLAlchemy ガイド](/blog/marshmallow-flask-sqlalchemy-rest-api-production-guide)の `Router → Schema → Model` 層分離と同じ思想です。smorest は Router 層に「ドキュメント自動化」を上乗せするだけで、層の責務分離は崩しません。

---

## **10. まとめ：スキーマを書けば、契約が手に入る**

Flask-smorest の本質は、**「ビューとスキーマを書くこと」と「機械可読な契約を維持すること」を、同じ 1 つの行為に統合する**ことです。FastAPI が型ヒントで得る OpenAPI ドキュメントを、Flask は marshmallow スキーマで、WSGI のまま手に入れられます。「Flask だからドキュメントは手書き」という時代は終わりました。

要点を、最後にもう一度。

- **手書き Flask API には機械可読な契約がない**。それが乖離するドキュメント・口頭での仕様伝達・破壊的変更の見落としを生む
- **Flask-smorest は Flask + marshmallow + webargs + apispec の束**。1 つのスキーマが入力検証・出力整形・OpenAPI 仕様を同時に駆動する（契約の単一情報源）
- **`@blp.arguments` / `@blp.response` / `@blp.paginate` / `abort`** で、検証・整形・ページング・エラーを宣言的に書け、すべてが自動で文書化される
- **本番では Swagger UI を環境ごとに保護**し、**CI で openapi.json を静的生成**してクライアントの型安全に繋ぐ
- **手段は smorest / APIFlask / 手書き / FastAPI** から、ランタイム（WSGI/ASGI）とスキーマ資産（marshmallow/Pydantic）で選ぶ

### **OpenAPI / ドキュメント自動化チェックリスト**

| # | 項目 | 確認内容 |
|---|---|---|
| 1 | 必須設定 | `API_TITLE` / `API_VERSION` / `OPENAPI_VERSION` を設定したか |
| 2 | import 元 | `Blueprint` / `abort` を **`flask_smorest`** から import しているか（`flask` からではない） |
| 3 | 入力境界 | すべての入力に `@blp.arguments` を付け、`location` を明示したか |
| 4 | 出力境界 | すべてのレスポンスに `@blp.response(code, Schema)`、リストは `many=True` |
| 5 | 読み取り専用 | サーバ採番属性を `dump_only` にし、マスアサインメントを防いだか |
| 6 | Response 返却の罠 | `Response` を返すと dump されない点を理解し、整形したい箇所では dict/ORM を返しているか |
| 7 | ページネーション | 一覧に `@blp.paginate()`、`item_count` を設定、`X-Pagination` を文書化したか |
| 8 | エラー | `abort(code, message=...)` で一貫エラー、422/404 が仕様に載っているか |
| 9 | 仕様の配信 | `OPENAPI_URL_PREFIX` を設定したか（既定 None では配信されない） |
| 10 | UI の有効化 | `OPENAPI_SWAGGER_UI_PATH` / `OPENAPI_REDOC_PATH` を用途で設定したか |
| 11 | 本番保護 | 本番で Swagger UI を無効化 / 認証ゲートしたか（環境ごとに切替） |
| 12 | バージョニング | `url_prefix` または複数 `Api` でバージョンを表現したか |
| 13 | ドキュストリング | 各ビューに summary になるドキュストリング、Blueprint に `description` |
| 14 | CI 連携 | CI で openapi.json を静的生成し、クライアント型生成に繋いだか |
| 15 | スキーマ選定 | marshmallow（smorest）か Pydantic（APIFlask）か、資産に合わせて選んだか |

Flask は「核だけ」のフレームワークです。だからこそ、何を載せるかで本番品質が決まります。OpenAPI ドキュメントの自動化は、**載せる価値が極めて高い「核の外側」**です。スキーマを 1 つ書けば、検証も整形もドキュメントも、すべてが 1 つの真実から流れ出す——その規律が、別チームに渡せる、信頼できる API を作ります。
