# Auto-Generating OpenAPI/Swagger in Flask: Building Schema-Driven REST APIs and API Docs at Production Quality with Flask-smorest

> An implementation guide to auto-generating OpenAPI/Swagger with Flask-smorest 0.47. Bundle Flask + marshmallow + webargs + apispec to simultaneously generate input validation, response shaping, and the OpenAPI spec from a single schema. @blp.arguments/@blp.response, Swagger UI/ReDoc, pagination, error documentation, protecting Swagger UI in production, and generating openapi.json in CI — explained with real code.

- Published: 2026-06-26
- Author: 友田 陽大
- Tags: Python, Flask, OpenAPI, Swagger, REST API, marshmallow, アーキテクチャ設計
- URL: https://tomodahinata.com/en/blog/flask-openapi-swagger-flask-smorest-api-documentation-guide
- Category: Flask in production
- Pillar guide: https://tomodahinata.com/en/blog/flask-production-guide

## Key points

- A hand-written Flask API has no machine-readable contract. FastAPI gets OpenAPI/Swagger for free, but Flask-smorest gets the same value while staying WSGI/Flask
- Flask-smorest is a bundle of Flask + marshmallow + webargs + apispec. A single marshmallow schema simultaneously drives input validation, response shaping, and the OpenAPI spec (the contract's single source of truth)
- @blp.arguments(Schema) injects validated arguments, @blp.response(code, Schema) serializes the return value and sets the status. 422 errors are auto-documented too
- @blp.paginate() for pagination (the X-Pagination header), abort() for documented consistent errors, url_prefix to express versioning
- In production, protect Swagger UI with an auth gate / disabling, and statically generate openapi.json in CI to connect to client code-generation / type safety. Explained grounded in Flask REST API knowledge from a Minister of Economy, Trade and Industry Award-winning B2B SaaS

---

## **Introduction: Your Flask API Has No "Contract"**

Picture handing a hand-written Flask REST API to another team (front end, mobile, an external partner). The first thing they ask is always this: "What's this endpoint's request body? What shape is the response? What comes back on an error?"

And where is the answer? In many Flask projects, the answer is **reading the view-function code, asking on Slack, or sharing a Postman collection** — all "substance-less documentation" maintained by hand, separate from the code. The code changes but the docs don't. Eventually the two diverge, and the docs become "lying documentation." This is technical debt itself.

The essence of the problem is that **a hand-written Flask API has no machine-readable contract**. Even if you carefully do [Flask REST API design (MethodView / Blueprint / versioning)](/blog/flask-rest-api-design-methodview-blueprint-versioning-guide), that design exists only in code humans read, not in a spec (OpenAPI doc) the API's consumers can reference mechanically.

Here, many recall FastAPI. As touched on in [Flask vs FastAPI vs Django](/blog/flask-vs-fastapi-vs-django-comparison-guide), FastAPI generates **the OpenAPI spec and Swagger UI "for free"** from type hints. This is FastAPI's decisive appeal, and the representative example of what Flask is considered "not to have."

This article's claim is simple. **That gap is closed if you use `Flask-smorest` on the Flask side.** Staying WSGI / Flask, auto-generate the OpenAPI spec, Swagger UI, and ReDoc from a single marshmallow schema, with that same schema also handling input validation and response shaping — what FastAPI gets with types, Flask gets with a marshmallow schema.

The author designed, implemented, and ran in production the backend of a B2B SaaS that won the Minister of Economy, Trade and Industry Award in **Python / Flask / SQLAlchemy / PostgreSQL**. Providing a "machine-readable contract" to the in-house front-end team, and to external partners hitting the API, supported both development speed and trust. This article systematizes the "documentation automation" design that was needed in that field experience, with real code faithful to the Flask-smorest official documentation.

> 💡 **The version covered in this article**: it assumes **Flask-smorest 0.47.0**. Flask-smorest is "a REST API framework based on Flask / Marshmallow," with dependencies **flask>=3.0.2,<4 / marshmallow>=3.24.1,<5 / webargs>=8 / apispec[marshmallow]>=6**, requiring **Python 3.10 or above**. This article's code is based on the official documentation's patterns. The design of marshmallow schemas themselves is treated as prerequisite knowledge in the [marshmallow practical guide](/blog/marshmallow-python-serialization-validation-production-guide).

---

## **1. What Is Flask-smorest: A "Schema-Driven Core" Bundling 4 Libraries**

Flask-smorest is not an API framework rebuilt from scratch. It's **a thin adhesive layer that bundles 4 already-mature libraries and "drives them with a single schema."**

| Component | Role | Its positioning in smorest |
|---|---|---|
| **Flask** | WSGI application / routing | The foundation. Loaded as an extension with `Api(app)` |
| **marshmallow** | Validation / serialization via schemas | **The contract's single source of truth** (input, output, and spec) |
| **webargs** | Request parsing (query/json/path…) | The substance of `@blp.arguments` |
| **apispec** | Converting marshmallow schemas → the OpenAPI spec | The engine of doc auto-generation |

What this composition means is that **a single marshmallow schema does 3 jobs at once**.

1. **Validating the input boundary** (`load` via webargs): validate the JSON / query that came from the client, and auto-return 422 if invalid
2. **Shaping the output boundary** (`dump`): shape the return value with the schema, preventing the leakage of internal attributes
3. **Generating the OpenAPI spec** (via apispec): generate the request-body / response JSON Schema from the same schema and display it in Swagger UI

In the [marshmallow × Flask × SQLAlchemy guide](/blog/marshmallow-flask-sqlalchemy-rest-api-production-guide), I argued that "a single schema protects the 2 boundaries of entrance and exit." Flask-smorest adds a **3rd boundary there — machine-readable documentation — at zero additional cost.** This is the apex of DRY. Rewrite the schema, and validation, shaping, and documentation **follow simultaneously**, so there's no gap to diverge in the first place.

> 💡 **A shift in thinking**: FastAPI makes "Pydantic type hints" the single source of truth to generate OpenAPI. Flask-smorest makes "the marshmallow schema" the single source of truth to do the same thing. **Only the means (type hints vs schema objects) differs; the value obtained is isomorphic.** For a Flask project that has already invested in marshmallow, you can divert that investment to OpenAPI docs without migrating to ASGI.

---

## **2. Reading the Quickstart Closely: 4 New Concepts**

The official quickstart is short, but the elements for understanding Flask-smorest are condensed into it. First look at the whole, then dissect it line by line.

```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)
```

Here, 4 concepts specific to Flask-smorest appear. Let me look at them in order.

### **2.1 `Api(app)`: Loading It as an Extension**

With `api = Api(app)`, you attach Flask-smorest to the app as an extension. This `Api` object is the command tower that holds apispec internally, scans the registered Blueprints to assemble the OpenAPI spec, and serves the Swagger UI / ReDoc endpoints. To combine with an application factory, you can use `api.init_app(app)` per the `init_app` pattern covered in the [Flask production-operations guide](/blog/flask-production-guide).

> ⚠️ The 3 of `API_TITLE` / `API_VERSION` / `OPENAPI_VERSION` are **mandatory**. Without them, `Api(app)` fails at startup. `API_VERSION` (your API's version, e.g. `v1`) and `OPENAPI_VERSION` (the version of the OpenAPI spec itself, e.g. `3.0.2`) are different things, so don't conflate them.

### **2.2 smorest's `Blueprint`: Not Flask's Standard Blueprint**

The first pitfall is here.

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

This `Blueprint` is **not `flask.Blueprint` but Flask-smorest's own Blueprint**. It inherits Flask's standard Blueprint and additionally has OpenAPI-aware decorators (`@blp.arguments` / `@blp.response` / `@blp.paginate` …). Its constructor takes `description=`, and this becomes the OpenAPI tag description.

Similarly, `abort` is **not `flask.abort` but smorest's enhanced `abort`**. You can put an error message and additional info into the JSON error response (detailed in §5).

> ⚠️ **Anti-pattern**: mixing `from flask import Blueprint, abort` and `from flask_smorest import Blueprint, abort`. Use the former and `@blp.arguments` doesn't exist, giving an `AttributeError`. In files using Flask-smorest, import Blueprint / abort **always from `flask_smorest`**.

### **2.3 `@blp.route("/")` Decorates a Class**

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

Flask-smorest treats **class-based views (`MethodView`) as first-class**. `@blp.route("/")` decorates **the whole class**, and the `get` / `post` methods within the class map to HTTP GET / POST respectively. `MethodView` is a Flask-core class (`flask.views.MethodView`), not smorest's own.

Because you can bundle multiple HTTP methods for one URL into one class, it naturally meshes with resource-oriented REST design. The design philosophy of `MethodView` itself (resource = class, method = HTTP verb) is covered in detail in the [Flask REST API design guide](/blog/flask-rest-api-design-methodview-blueprint-versioning-guide). This article layers "documentation automation" onto that design.

### **2.4 The Order of Decorators and the Docstring**

Notice the 2 decorators on the method, and the docstring below them.

```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)`: validate the request body with `PetSchema`, and inject the validated dict as the `new_data` argument (§3)
- `@blp.response(201, PetSchema)`: serialize the return value `item` with `PetSchema`, and set HTTP 201 (§4)
- `"""Add a new pet"""`: **the docstring becomes the OpenAPI operation summary**. The source of the description shown in Swagger UI

With these 3 in place, `POST /pets` is automatically loaded into the docs as a machine-readable spec: "the request body is PetSchema, on success returns 201 with PetSchema, the description is Add a new pet." That **writing the view itself is writing the documentation** is the core of Flask-smorest.

---

## **3. `@blp.arguments`: Validating and Injecting the Input Boundary**

`@blp.arguments` is the protagonist of Flask-smorest's entrance side. It does 2 things.

1. Take the request data from the specified **location** and **validate (`load`)** it with the schema
2. **Inject** the validated data into the view function **as an argument**

If validation fails, Flask-smorest auto-returns **422 Unprocessable Entity**. You don't need to write `try/except ValidationError` inside the view function — boundary validation is declaratively externalized.

### **3.1 location: Where to Read From**

The `location` of `@blp.arguments(Schema, location=...)` specifies where the data is taken from.

| location | Source | Use |
|---|---|---|
| `json` (default) | The request body's JSON | POST / PUT payloads |
| `query` | The query string | List filtering / search parameters |
| `path` | URL path parameters | Resource IDs |
| `form` | Form data | HTML form submissions |
| `headers` | Request headers | Validating custom headers |
| `cookies` | Cookies | — |
| `files` | Uploaded files | Multipart |
| `json_or_form` | JSON or form | Endpoints supporting both |

Omit `location` and `json` is the default. When you want to validate query parameters, explicitly state `location="query"`, like the quickstart's GET.

### **3.2 How It's Injected: Positional Args / Keyword Args / Stacking**

By default, `@blp.arguments` injects the validated data as **a single positional argument (a dict)**.

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

Pass `as_kwargs=True` and it's injected expanded as **`**kwargs`** rather than a dict.

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

**Stacking** multiple `@blp.arguments` injects multiple positional arguments in declaration order. This is effective when you want to validate query and json simultaneously.

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

> 💡 The argument order of stacking corresponds to the decorator order "top to bottom." For readability, when the locations differ, give variable names that **make the source clear**, like `query_args` / `body`, so a later reader doesn't get lost.

### **3.3 422 Is "Auto-Documented"**

This is the decisive difference from a hand-written API. An endpoint with `@blp.arguments` has **a validation-error (422) response automatically added to the OpenAPI spec**. That is, the API's consumer can know mechanically, just by looking at Swagger UI, that "send invalid input and 422 comes back, and the error's shape is this." Hand-written, the existence and shape of this 422 was "tacit knowledge you can't know without reading the code." Flask-smorest turns it into an explicit contract.

---

## **4. `@blp.response`: Shaping the Output Boundary and Setting the Status**

`@blp.response(status_code, Schema)` is the protagonist of the exit side. It does 3 things.

1. **`dump`** (serialize) the view function's **return value with the schema**
2. **Set the HTTP status code**
3. **Register** that response (status + schema) **in the OpenAPI spec**

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

When returning a list, use `Schema(many=True)`. `many=True` is reflected in the OpenAPI spec too, correctly documenting "the response is an array."

The power of `dump_only` fields (e.g. `id = ma.fields.Int(dump_only=True)`) also works here. `id` is not accepted on input (`@blp.arguments`) — preventing mass assignment — and is returned on output (`@blp.response`); you can declare a "read-only attribute" with one schema. This input/output-asymmetric design is as detailed in the [marshmallow serialization/validation guide](/blog/marshmallow-python-serialization-validation-production-guide), and smorest translates it directly to OpenAPI's `readOnly`.

> ⚠️ **An important exception (official caution)**: if the view function returns a `werkzeug.BaseResponse` (= a `Response` object or the result of `make_response()`), **that Response is returned as-is, and neither schema `dump` nor status-code application happens.** For cases where you build a Response directly, like a file download or a redirect, understand that `@blp.response`'s schema shaping doesn't take effect. If you want schema shaping, the iron rule is to **return a dict or an ORM object** (don't make a Response).

---

## **5. Pagination and Errors: Turning Lists and Abnormal Cases into "Documented Contracts"**

What most tends to cause "spec divergence" in a practical REST API is **lists (pagination) and abnormal cases (errors)**. Flask-smorest has mechanisms to fix both as contracts.

### **5.1 `@blp.paginate()`: Declaring Pagination**

```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()` injects a `PaginationParameters` object into the view. What you get from it, and what you should do, is as follows.

- **Injected**: `pagination_parameters` (`.page` / `.page_size`, the computed `.first_item` / `.last_item`)
- **You set**: `pagination_parameters.item_count = <total count>` (needed to compute the total page count)
- **Auto-attached**: the pagination metadata rides in the **`X-Pagination` response header**

The default parameters are `DEFAULT_PAGINATION_PARAMETERS = {"page": 1, "page_size": 10, "max_page_size": 100}`. The `page` / `page_size` query parameters and their defaults / upper bound (`max_page_size`) are also auto-documented in the OpenAPI spec. The client, just by looking at Swagger UI, knows mechanically "hit it with `?page=2&page_size=50`, with an upper bound of 100."

> 💡 Putting pagination info in the **`X-Pagination` header** rather than the **body** is a design judgment. The response body is purified to "an array of resources," and the metadata (total count, whether there's a next page) is separated into the header. It's easier for the client to parse the metadata, and the body's schema isn't polluted by meta.

### **5.2 `abort()`: Documented Consistent Errors**

Return abnormal cases with smorest's `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's `abort` extends `flask.abort` and **can include additional info like `message` in the JSON error response**. Flask-smorest shapes the error response into a consistent form (`code` / `status` / `message` / `errors` on validation errors), and moreover registers that form in the OpenAPI spec. That is, **"this endpoint can return 404, and the error's shape is this" rides in the contract.**

This "documented consistent error envelope," done by hand, will surely sprawl. The "unified JSON errors across all endpoints" argued in the [Flask error-handling / observability guide](/blog/flask-error-handling-logging-observability-guide), smorest provides as standard, and documented to boot.

| Abnormal case | Hand-written Flask | Flask-smorest |
|---|---|---|
| Validation error (422) | Hand-write `try/except` everywhere | `@blp.arguments` auto 422 + documentation |
| Not Found (404) | Scatter `jsonify(...), 404` | Consistent + documented with `abort(404, message=...)` |
| Error shape | Different per endpoint | Unified envelope + registered in OpenAPI |
| Conveying to consumers | Read the code / verbally | Machine-readable in Swagger UI |

---

## **6. OpenAPI Settings and UI Serving: Enabling Swagger UI / ReDoc**

So far you've understood that "the spec is auto-generated." Next is **where and how to serve that spec**. Flask-smorest can serve the OpenAPI JSON, Swagger UI, and ReDoc with config alone.

```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"
```

There are 3 default-value behaviors to pin down.

1. **`OPENAPI_URL_PREFIX`'s default is `None`.** Unless you set this, **the OpenAPI spec itself isn't served**. Only after setting it to `"/"` or the like does `/openapi.json` become visible.
2. **`OPENAPI_SWAGGER_UI_PATH`'s default is also `None`.** Only after setting it does Swagger UI become enabled. In the example above, you can see the interactive UI at `/swagger-ui`.
3. **`OPENAPI_REDOC_PATH`'s default is also `None`.** Only after setting it does ReDoc become enabled. ReDoc is read-only, easy-to-read three-column documentation.

> 💡 **When to use Swagger UI vs ReDoc**: Swagger UI is "**hittable**" (with Try it out you can send real requests from the browser), so it's powerful in the development / verification phase. ReDoc is "**readable**" (beautiful static docs), so it suits external publication / reference use. The standard is to enable both and use them as appropriate. The default configuration loads the UI's JS assets from a CDN (jsDelivr).

---

## **7. Production-Operation Manners: Protection, Versioning, Code Generation in CI**

There's a chasm between "works" and "production quality." Let me list the points I always pin down when shipping Flask-smorest to production.

### **7.1 How to Handle Swagger UI in Production**

Swagger UI, handy in development, **should sometimes not be exposed as-is in production**. There are 2 reasons. (a) Fully exposing the internal API's endpoints, schemas, and parameters hands over a map of the attack surface; (b) with "Try it out," destructive operations against production data can be tried. The countermeasures can be chosen in stages.

| Strategy | Method | Suited case |
|---|---|---|
| **Full disabling** | Don't set `OPENAPI_SWAGGER_UI_PATH` in the production config (`None`) | A fully internal API, no UI needed |
| **Auth gate** | Place the UI-serving path behind Basic auth or an IP restriction via a reverse proxy / extension | Want it viewable only internally |
| **Serve JSON only** | Serve `openapi.json` but not the UI | Consumers read it with their own tools |
| **Full publication** | Publish including the UI | Public APIs, partner portals |

The crux is that the config **switches per environment**. Per the "12-factor-izing of config" covered in the [Flask production-operations guide](/blog/flask-production-guide), inject `OPENAPI_SWAGGER_UI_PATH` from environment variables / instance config, and switch like disabled in production, enabled in development. Don't write a fixed value in the code.

> ⚠️ "Exposing Swagger UI in production = an instant security incident" is not true. **For an auth-required API, Try it out also requires auth**, so it's not unconditionally dangerous. That said, the cost of "handing out a map of the attack surface for free" is always there, so **unless it's a public API, make an auth gate or disabling the default** — that's the safe-side judgment.

### **7.2 Versioning: `url_prefix` or Multiple `Api`s**

API versioning can be expressed straightforwardly with smorest's 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)
```

Register multiple-version Blueprints to the same `Api`, and v1 / v2 line up in one OpenAPI spec. If you want to **completely separate the specs** per version, you can also take the configuration of standing up multiple `Api` instances and serving each at a different `OPENAPI_URL_PREFIX`. The design judgment of URL-path versioning (path vs header vs media type) I leave to the [Flask REST API design guide](/blog/flask-rest-api-design-methodview-blueprint-versioning-guide).

### **7.3 Statically Generating `openapi.json` in CI to Connect to Client Type Safety**

This is where Flask-smorest's investment **pays off the most**. The OpenAPI spec shouldn't end as just "documentation humans read" — that's a waste. You can make it the input for **machines to read and auto-generate a typed client**.

In CI, write the OpenAPI spec out to a static file. You can get the spec from Flask-smorest's `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
```

Once this flow is complete, a type-safe pipeline forms: **a backend schema change → openapi.json updated in CI → client types regenerated → breaking changes detected immediately as compile errors**. The contract between server and client is guaranteed by **the type system**, not by human attentiveness. The whole picture of this "end-to-end type safety centered on OpenAPI" is argued in detail in the [Next.js × Go end-to-end type-safety guide](/blog/nextjs-go-openapi-end-to-end-type-safety). Even if the backend is Flask, you can enjoy the same type-safety benefit by going through OpenAPI.

> 💡 **The hygiene of tags / operationId**: an auto-generated client's method names derive from OpenAPI's `operationId`. Give each Blueprint a meaningful name and `description`, and write a clear docstring per endpoint, and the generated client's code becomes readable. Be conscious that "the quality of the docs = the quality of the client code."

---

## **8. Technology Selection: An Honest Comparison of smorest / APIFlask / Hand-Written / FastAPI**

Flask-smorest isn't the only right answer. There are multiple means to obtain OpenAPI docs, each with its right place. Following the principle that [technology selection is a matter of "fit," not "superiority"](/blog/flask-vs-fastapi-vs-django-comparison-guide), let me show an honest comparison.

### **8.1 The Option of APIFlask**

A strong alternative to `Flask-smorest` is **APIFlask 3.1.1**. It's "a lightweight Flask-based Web API framework," and like smorest auto-generates the OpenAPI spec, Swagger UI, and ReDoc. The biggest difference is that **its schema adapter is swappable, handling both marshmallow schemas and Pydantic models** (Pydantic support is a relatively new feature added in the 3.x line).

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

If you've already invested in Pydantic, or want to hold your schema assets in Pydantic looking ahead to a future FastAPI migration — in such cases APIFlask's Pydantic support works. Conversely, if you've already solidified your boundary design with marshmallow (many readers of this cluster have), the marshmallow-native smorest is straightforward.

### **8.2 A Decision Table: When to Choose What**

| Option | OpenAPI auto-generation | Schema | Runtime | When to choose |
|---|---|---|---|---|
| **Hand-written Flask** | None (write it yourself) | Apply marshmallow etc. by hand | WSGI | An extremely small API of 1–2 endpoints. No docs needed |
| **Flask-smorest** | **Yes** (automatic) | marshmallow | WSGI | Want a contract while leveraging existing Flask + marshmallow assets |
| **APIFlask** | **Yes** (automatic) | marshmallow **or Pydantic** | WSGI | Want to use Pydantic while staying Flask / looking ahead to migration |
| **FastAPI** | **Yes** (standard) | Pydantic (type hints) | ASGI | New, high-concurrency IO, type-hint-driven, want to use async fully |

The decision can be organized thus.

- **A new project that can go to ASGI, where you want to maximize type-hint-driven async → FastAPI**. OpenAPI comes standard.
- **You can't move off existing Flask (WSGI), or don't want to → Flask-smorest / APIFlask**. Get OpenAPI while staying WSGI.
- **Within that, smorest if you have marshmallow assets, APIFlask if you want to write in Pydantic.**
- **An extremely small internal API needing no contract in the first place → hand-written.** But beware the premise that "no contract needed" crumbles over time. The moment endpoints exceed 3 and consumers become a separate team, the value of bringing in smorest stands up.

> 💡 **"Flask, so OpenAPI is impossible" is a misunderstanding.** FastAPI's OpenAPI auto-generation is certainly powerful, but it's not something "obtainable only with FastAPI." Flask-smorest / APIFlask provide equivalent value while staying WSGI. Weigh the cost of a framework migration (WSGI → ASGI) and the value of documentation automation on **separate scales**. "Migrate to FastAPI because I want OpenAPI" is, in many cases, overkill.

---

## **9. A Worked Example: A Documented `/api/v1/orders` Resource**

Let me land the theory in a form actually needed in a B2B SaaS. I assemble **listing and creating orders** as a single resource complete with validation, shaping, pagination, errors, and documentation.

### **9.1 The Schema: The Contract's Single Source of Truth**

```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` are **`dump_only`** — read-only attributes the server numbers / manages, which the client can't input (preventing mass assignment). `customer_id` / `amount` are **`required`**, auto-422 if missing. This single schema simultaneously drives input validation, output shaping, and the OpenAPI spec.

### **9.2 The Resource: View = Documentation**

```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)
```

Read the contract this code generates.

- `GET /api/v1/orders`: filterable in query by `customer_id` / `status`, pageable with `page` / `page_size` (default 10, upper bound 100), returns an array of Order with 200, total count in the `X-Pagination` header
- `POST /api/v1/orders`: the body is OrderSchema (`customer_id` / `amount` required, `id` etc. not accepted), returns Order with 201 on success, 422 on validation failure, 422 on a business error (with message)

**All of these are automatically loaded into Swagger UI.** The front-end team can "Try it out" in the UI to try creating an order, an external partner reads the spec in ReDoc, and CI generates a typed client from openapi.json. In my B2B SaaS, being able to provide this "hittable contract" accelerated everything — the in-house front-end implementation, partner integration, and sales' technical explanations. **The effort to write documentation separately is zero** — because writing the schema and the view becomes the contract itself.

> 💡 Notice that business logic is offloaded to the Service layer (`OrderService`), and the view sticks to a thin conversion of "HTTP ↔ schema ↔ service." This is the same thought as the `Router → Schema → Model` layered separation of the [marshmallow × Flask × SQLAlchemy guide](/blog/marshmallow-flask-sqlalchemy-rest-api-production-guide). smorest merely layers "documentation automation" onto the Router layer; it doesn't break the layers' separation of responsibilities.

---

## **10. Summary: Write a Schema, and You Get a Contract**

The essence of Flask-smorest is **integrating "writing views and schemas" and "maintaining a machine-readable contract" into the same single act.** The OpenAPI docs FastAPI gets with type hints, Flask can get with a marshmallow schema, while staying WSGI. The era of "Flask, so docs are hand-written" is over.

The key points, one more time at the end.

- **A hand-written Flask API has no machine-readable contract.** That produces diverging docs, verbal spec-conveyance, and missed breaking changes
- **Flask-smorest is a bundle of Flask + marshmallow + webargs + apispec.** A single schema simultaneously drives input validation, output shaping, and the OpenAPI spec (the contract's single source of truth)
- With **`@blp.arguments` / `@blp.response` / `@blp.paginate` / `abort`**, you can write validation, shaping, paging, and errors declaratively, and all of it is auto-documented
- **In production, protect Swagger UI per environment**, and **statically generate openapi.json in CI** to connect to client type safety
- **Choose the means** from smorest / APIFlask / hand-written / FastAPI, by runtime (WSGI/ASGI) and schema assets (marshmallow/Pydantic)

### **An OpenAPI / Documentation-Automation Checklist**

| # | Item | What to confirm |
|---|---|---|
| 1 | Mandatory config | Did you set `API_TITLE` / `API_VERSION` / `OPENAPI_VERSION`? |
| 2 | Import source | Are you importing `Blueprint` / `abort` from **`flask_smorest`** (not from `flask`)? |
| 3 | Input boundary | Did you attach `@blp.arguments` to all inputs and state `location` explicitly? |
| 4 | Output boundary | `@blp.response(code, Schema)` on all responses, `many=True` for lists |
| 5 | Read-only | Did you make server-numbered attributes `dump_only` and prevent mass assignment? |
| 6 | The Response-return trap | Do you understand that returning a `Response` skips dump, and return dict/ORM where you want shaping? |
| 7 | Pagination | `@blp.paginate()` on lists, set `item_count`, documented `X-Pagination`? |
| 8 | Errors | Consistent errors with `abort(code, message=...)`, are 422/404 in the spec? |
| 9 | Serving the spec | Did you set `OPENAPI_URL_PREFIX` (with default None it isn't served)? |
| 10 | Enabling the UI | Did you set `OPENAPI_SWAGGER_UI_PATH` / `OPENAPI_REDOC_PATH` by use? |
| 11 | Production protection | Did you disable / auth-gate Swagger UI in production (switching per environment)? |
| 12 | Versioning | Did you express versions with `url_prefix` or multiple `Api`s? |
| 13 | Docstring | A summary-making docstring on each view, a `description` on the Blueprint |
| 14 | CI integration | Did you statically generate openapi.json in CI and connect it to client type generation? |
| 15 | Schema selection | Did you choose marshmallow (smorest) or Pydantic (APIFlask) to match your assets? |

Flask is a "core-only" framework. That's exactly why what you load onto it decides production quality. Automating OpenAPI docs is **an "outside-the-core" of extremely high value to load**. Write one schema, and validation, shaping, and documentation all flow from one truth — that discipline makes a trustworthy API you can hand to another team.
