Chuyển đến nội dung chính
友田 陽大
Flask in production
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生成までを実コードで解説します。

Published
Reading time
26 min read
Author
友田 陽大
Chia sẻ
Mục lục

導入:あなたの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 を作ります。

友田

友田 陽大

Người phát triển sản phẩm đoạt Giải thưởng Bộ trưởng (METI). Với TypeScript + Python + AWS, tôi một mình triển khai trọn gói SaaS, chuyển đổi số công nghiệp và AI tạo sinh (RAG) sẵn sàng cho production — từ phân tích yêu cầu đến hạ tầng và vận hành.

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

Nhận cả dự án trọn gói lẫn vai trò cố vấn kỹ thuật. Hãy bắt đầu bằng buổi tư vấn miễn phí 30 phút.

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

Cũng đáng đọc