導入:あなたのFlask APIには「契約」がない
手で書いた Flask の REST API を、別チーム(フロントエンド、モバイル、外部パートナー)に渡す場面を思い浮かべてください。彼らが最初に聞くのは決まってこうです。「このエンドポイントのリクエストボディは何?レスポンスはどんな形?エラーのときは何が返る?」
そして、その答えはどこにあるでしょうか。多くの Flask 案件では、答えは ビュー関数のコードを読むか、Slack で聞くか、Postman のコレクションを共有するか——いずれも、コードとは別に手で維持される「実体のないドキュメント」です。コードが変わってもドキュメントは変わらない。やがて両者は乖離し、ドキュメントは「嘘をつくドキュメント」になります。これは技術的負債そのものです。
問題の本質は、手書きの Flask API には機械可読な契約(contract)がないことです。Flask の REST API 設計(MethodView / Blueprint / バージョニング)を丁寧に行っても、その設計は人間が読むコードの中にしか存在せず、API の利用者が機械的に参照できる仕様(OpenAPI ドキュメント)にはなりません。
ここで多くの人が思い出すのが FastAPI です。Flask vs FastAPI vs Django の比較でも触れたとおり、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 実践ガイド を前提知識とします。
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 つの仕事を同時にこなすということです。
- 入力境界の検証(webargs 経由で
load):クライアントから来た JSON / query を検証し、不正なら自動で 422 を返す - 出力境界の整形(
dump):戻り値をスキーマで整形し、内部属性の漏洩を防ぐ - OpenAPI 仕様の生成(apispec 経由):同じスキーマから request body / response の JSON Schema を生成し、Swagger UI に表示する
marshmallow × Flask × SQLAlchemy のガイドでは、「1 つのスキーマが入口と出口の 2 つの境界を守る」ことを論じました。Flask-smorest はそこに 3 つ目の境界——機械可読なドキュメント——を、追加コストゼロで足します。これが DRY の極致です。スキーマを書き換えれば、検証・整形・ドキュメントが同時に追従するので、乖離する隙間がそもそも存在しません。
💡 発想の転換:FastAPI は「Pydantic の型ヒント」を単一情報源にして OpenAPI を生成します。Flask-smorest は「marshmallow スキーマ」を単一情報源にして同じことをします。手段(型ヒント vs スキーマオブジェクト)が違うだけで、得られる価値は同型です。すでに marshmallow に投資している Flask プロジェクトなら、ASGI へ移行せずに、その投資を OpenAPI ドキュメントへ転用できます。
2. クイックスタートを精読する:4 つの新しい概念
公式クイックスタートは短いですが、ここには Flask-smorest を理解するための要素が凝縮されています。まず全体を見てから、1 行ずつ解剖します。
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 本番運用ガイドの 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 ではない
最初の落とし穴がここです。
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("/") はクラスを装飾する
@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 設計ガイドで詳しく扱っています。本記事はその設計に「ドキュメント自動化」を重ねます。
2.4 装飾子の順序とドキュストリング
メソッドに付く 2 つの装飾子と、その下のドキュストリングに注目してください。
@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 つ。
- 指定された location からリクエストデータを取り出し、スキーマで**検証(
load)**する - 検証済みのデータを、ビュー関数に引数として注入する
検証に失敗すれば、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) として注入します。
@blp.arguments(PetSchema)
def post(self, new_data): # new_data は検証済み dict
...
as_kwargs=True を渡すと、dict ではなく **kwargs として展開注入されます。
@blp.arguments(PetSchema, as_kwargs=True)
def post(self, name, **kwargs): # スキーマのフィールドが個別のキーワード引数に
...
複数の @blp.arguments を スタッキングすると、宣言順に複数の位置引数が注入されます。query と json を同時に検証したい場合に有効です。
@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 つ。
- ビュー関数の戻り値をスキーマで
dump(シリアライズ)する - HTTP のステータスコードを設定する
- そのレスポンス(ステータス + スキーマ)を OpenAPI 仕様に登録する
@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 ガイドで詳述したとおりで、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():ページネーションを宣言する
@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 で返します。
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 のエラー処理・可観測性ガイドで論じた「全エンドポイントで統一された 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 を、設定だけで配信できます。
# 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 つです。
OPENAPI_URL_PREFIXの既定はNone。これを設定しない限り、OpenAPI 仕様自体が配信されません。"/"などに設定して初めて、/openapi.jsonが見えるようになります。OPENAPI_SWAGGER_UI_PATHの既定もNone。設定して初めて Swagger UI が有効になります。上記の例なら/swagger-uiでインタラクティブな UI が見られます。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 本番運用ガイドで扱った「設定の 12-factor 化」に従い、OPENAPI_SWAGGER_UI_PATH を環境変数 / インスタンス設定から注入し、本番では無効・開発では有効、のように切り替えます。コードに固定値で書かないこと。
⚠️ 「Swagger UI を本番に出す = 即セキュリティ事故」ではありません。認証必須の API なら、Try it out も認証が要るので、無条件に危険とは限りません。とはいえ「攻撃面の地図を無料で配る」コストは常にあるので、公開 API でない限りは認証ゲートか無効化を既定にするのが安全側の判断です。
7.2 バージョニング:url_prefix か 複数 Api か
API のバージョニングは、smorest の Blueprint の url_prefix で素直に表現できます。
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 設計ガイドに譲ります。
7.3 CI で openapi.json を静的生成し、クライアント型安全に繋ぐ
ここが、Flask-smorest の投資が最も大きく報われるポイントです。OpenAPI 仕様は、ただ「人が読むドキュメント」で終わらせてはもったいない。機械が読んで、型付きのクライアントを自動生成するための入力にできます。
CI で OpenAPI 仕様を静的ファイルに書き出します。Flask-smorest の Api から仕様を取得できます。
# 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()
# 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 型安全ガイドで詳しく論じています。バックエンドが Flask でも、OpenAPI を経由すれば同じ型安全の恩恵を受けられます。
💡 tags / operationId の衛生:自動生成されたクライアントのメソッド名は OpenAPI の
operationId由来になります。各 Blueprint に意味のある名前・descriptionを与え、エンドポイントごとに明確なドキュストリングを書いておくと、生成されるクライアントのコードが読みやすくなります。「ドキュメントの質 = クライアントコードの質」だと意識してください。
8. 技術選定:smorest / APIFlask / 手書き / FastAPI の正直な比較
Flask-smorest が唯一の正解ではありません。OpenAPI ドキュメントを得る手段は複数あり、それぞれに適所があります。技術選定は「優劣」ではなく「適合」の問題だという原則に従い、正直な比較を示します。
8.1 APIFlask という選択肢
Flask-smorest の有力な代替が APIFlask 3.1.1 です。「Flask ベースの軽量 Web API フレームワーク」で、smorest と同様に OpenAPI 仕様・Swagger UI・ReDoc を自動生成します。最大の違いは、スキーマアダプタが差し替え可能で、marshmallow スキーマと Pydantic モデルの両方を扱える点です(Pydantic 対応は 3.x 系で入った比較的新しい機能です)。
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 スキーマ:契約の単一情報源
# 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 リソース:ビュー = ドキュメント
# 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 ガイドの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 を作ります。