メインコンテンツへスキップ
友田 陽大
Flask 本番運用
Python
Flask
OpenAPI
Swagger
REST API
marshmallow
アーキテクチャ設計

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生成までを実コードで解説します。

公開日
読了時間
26分
著者
友田 陽大
シェア
目次

導入:あなたの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]>=6Python 3.10 以上が必要です。本稿のコードは公式ドキュメントのパターンに基づきます。marshmallow スキーマそのものの設計は marshmallow 実践ガイド を前提知識とします。


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

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

構成要素役割smorest における位置づけ
FlaskWSGI アプリケーション・ルーティング土台。Api(app) で拡張として載せる
marshmallowスキーマによる検証・シリアライズ契約の単一情報源(入力も出力も仕様も)
webargsリクエストのパース(query/json/path…)@blp.arguments の中身
apispecmarshmallow スキーマ → 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 のガイドでは、「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

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

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

⚠️ アンチパターンfrom flask import Blueprint, abortfrom 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):戻り値 itemPetSchema でシリアライズし、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(既定)リクエストボディの JSONPOST / PUT のペイロード
queryクエリ文字列一覧の絞り込み・検索パラメータ
pathURL パスパラメータリソース ID
formフォームデータHTML フォーム送信
headersリクエストヘッダカスタムヘッダの検証
cookiesCookie
filesアップロードファイルマルチパート
json_or_formJSON またはフォーム両対応エンドポイント

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 つ。

  1. ビュー関数の戻り値をスキーマで dump(シリアライズ)する
  2. HTTP のステータスコードを設定する
  3. そのレスポンス(ステータス + スキーマ)を 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 の abortflask.abort を拡張し、message などの追加情報を JSON エラーレスポンスに含められます。Flask-smorest はエラーレスポンスを一貫した形(code / status / message / 検証エラー時は errors)に整え、しかもその形を OpenAPI 仕様に登録します。つまり 「このエンドポイントは 404 を返しうる、エラーの形はこうだ」が契約に載る

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

異常系手書き FlaskFlask-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 つです。

  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 本番運用ガイドで扱った「設定の 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 等を手で適用WSGI1〜2 エンドポイントの極小 API。ドキュメント不要
Flask-smorestあり(自動)marshmallowWSGI既存 Flask + marshmallow 資産を活かしつつ契約が欲しい
APIFlaskあり(自動)marshmallow or PydanticWSGIFlask のまま 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_atdump_only——サーバが採番・管理する読み取り専用属性で、クライアントは入力できません(マスアサインメント防止)。customer_id / amountrequired で、欠けていれば自動 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 を設定したか
2import 元Blueprint / abortflask_smorest から import しているか(flask からではない)
3入力境界すべての入力に @blp.arguments を付け、location を明示したか
4出力境界すべてのレスポンスに @blp.response(code, Schema)、リストは many=True
5読み取り専用サーバ採番属性を dump_only にし、マスアサインメントを防いだか
6Response 返却の罠Response を返すと dump されない点を理解し、整形したい箇所では dict/ORM を返しているか
7ページネーション一覧に @blp.paginate()item_count を設定、X-Pagination を文書化したか
8エラーabort(code, message=...) で一貫エラー、422/404 が仕様に載っているか
9仕様の配信OPENAPI_URL_PREFIX を設定したか(既定 None では配信されない)
10UI の有効化OPENAPI_SWAGGER_UI_PATH / OPENAPI_REDOC_PATH を用途で設定したか
11本番保護本番で Swagger UI を無効化 / 認証ゲートしたか(環境ごとに切替)
12バージョニングurl_prefix または複数 Api でバージョンを表現したか
13ドキュストリング各ビューに summary になるドキュストリング、Blueprint に description
14CI 連携CI で openapi.json を静的生成し、クライアント型生成に繋いだか
15スキーマ選定marshmallow(smorest)か Pydantic(APIFlask)か、資産に合わせて選んだか

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

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事で解説した技術の適用事例

経済産業大臣賞受賞 | 木材流通業界のDXを実現したB2BサブスクリプションSaaS

ケーススタディを見る