OpenAPI & API Documentation

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Flask 3.1.3 (released 2026-02-19), Werkzeug 3.1.9, flask-smorest 0.47.0 (2026-03-22), apispec 6.10.0, marshmallow 4.3.1, APIFlask 3.1.2 (2026-09-12) and Python 3.13, checked on 2026-10-07; every Python example was executed with those versions through Flask’s test client. Flask 3.2 is not released yet and does not change anything described here. An API that nobody can discover is an API nobody can use, and an API whose documentation is written by hand drifts away from the code within weeks. The cure is a machine-readable description, an OpenAPI document, that is the single source of truth: documentation pages, interactive consoles, client libraries, mocks, gateways and contract tests are all generated from it. The page covers what an OpenAPI document contains, the two ways of keeping it truthful (code-first, where the code produces the document, and design-first, where the document produces or constrains the code), how Flask does each, and how to publish, test and consume the result. It extends the Bookshelf API of the previous pages. Flask itself has no OpenAPI support: everything here comes from extensions, and the page says which library does which job.

Why Document an API with OpenAPI

Flask validates nothing and documents nothing by itself: a view receives a request and returns whatever it likes (see Request Data and Responses & JSON). That freedom is exactly why an API needs a contract. The OpenAPI Specification (OAS) is a language-neutral way of writing that contract as a JSON or YAML document: the paths, the operations on each path, the parameters, the request bodies, the responses, the data types and the security requirements. Because it is machine-readable, the same document feeds many tools.

Consumer of the document What it does with it

Documentation renderers (Swagger UI, ReDoc, RapiDoc)

Render interactive reference pages, with a "try it out" console in the case of Swagger UI and RapiDoc

Client generators

Produce typed client libraries and SDKs for Python, TypeScript and many other languages

Gateways, proxies and mock servers

Route, authenticate, rate-limit or simulate the API from the same description

Contract and property-based tests

Generate requests from the schemas and check every response against them; see the page on API contract and property-based testing in this section

Linters and governance tools

Enforce naming, error-format and versioning rules across many services

People

Review the API shape in a pull request before any code exists (documentation-driven or API-first development)

The API-first idea, argued at length in Microservice APIs (Manning), is that the interface between services is the part that is expensive to change, so it deserves a document that is reviewed, versioned and agreed before the implementation. Whether the document is written first or generated, the rule stays the same: the running service and the published document must never disagree.

Code-First and Design-First

There are two workflows, and Flask libraries exist for both.

flowchart LR subgraph CF["Code-first"] direction LR A["Python code
views, schemas, decorators"] --> B["library collects metadata
apispec or APIFlask"] B --> C["OpenAPI document"] end subgraph DF["Design-first"] direction LR D["hand-written YAML
reviewed in a pull request"] --> E["OpenAPI document"] E --> F["implementation checked
against the document"] end C --> G["docs UI, clients,
mocks, contract tests"] E --> G
Figure 1. Code-first generates the document from the code; design-first starts from the document
Code-first Design-first

The document is

generated; it is always in sync with the code that was introspected

written by hand; it must be kept in sync by tests

Flask tools

flask-smorest, APIFlask, flask-openapi3, Flask-RESTX (Swagger 2.0)

Connexion (routes the document to your functions), a hand-written file validated by tests, plus any generator

Strengths

no duplication, refactoring-friendly, validation and documentation come from one declaration

the contract can be reviewed and negotiated before coding, the document can carry richer prose and examples

Risks

the document is limited to what the library can express; it describes what the code does, not what was agreed

document and code drift unless a test fails when they differ

The rest of the page follows the code-first path with flask-smorest first, because it is the most complete option for plain Flask, then APIFlask, then the design-first path with a validated hand-written document. Both are only as good as the checks around them, which is why the page ends with contract validation.

Anatomy of an OpenAPI Document

An OpenAPI document is a single JSON or YAML object. The OpenAPI Specification 3.1 is aligned with JSON Schema draft 2020-12, so the objects that describe data (schema) are ordinary JSON Schema, with a few OpenAPI keywords added.

Top-level field Meaning

openapi

the version of the specification the document follows, for example 3.1.0 (required)

info

title, version of the API itself (not of the specification), description, contact, license (required)

servers

the base URLs where the API is available

paths

every URL template, each with its operations (get, post, put, patch, delete and so on)

components

reusable objects referenced with $ref: schemas, parameters, requestBodies, responses, headers, examples, securitySchemes, links, callbacks

security

the security requirements that apply to every operation unless an operation overrides them

tags, externalDocs

grouping and extra documentation

webhooks

incoming calls the API may receive (new in 3.1)

A document needs openapi and info, and at least one of paths, components or webhooks. Inside paths, each operation lists its parameters (each with a name, a location in in, either path, query, header or cookie, and a schema), an optional requestBody keyed by media type, and responses keyed by status code (or default). Every response has a mandatory description and optionally content and headers. The reusable parts live in components and are referenced with $ref, so that the same Book schema is written once and used by the request of POST /books and the response of GET /books/{bookId}. Security is declared once under components.securitySchemes (types apiKey, http, oauth2, openIdConnect and mutualTLS) and applied with security, which is how Swagger UI knows to show an "Authorize" button.

openapi: 3.1.0
info:
  title: Bookshelf API
  version: 1.0.0
paths:
  /books/{bookId}:
    get:
      operationId: getBook
      parameters:
        - name: bookId
          in: path
          required: true
          schema: {type: integer, minimum: 1}
      responses:
        "200":
          description: The book
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Book"}
components:
  schemas:
    Book:
      type: object
      required: [id, title]
      properties:
        id: {type: integer}
        title: {type: string, minLength: 1}
        subtitle: {type: [string, "null"]}

Which OpenAPI Version

OpenAPI 3.0 and 3.1 differ in ways that show up in generated documents.

Topic OpenAPI 3.0.x OpenAPI 3.1.x

Schema dialect

a modified subset of JSON Schema draft 4/5 (nullable, exclusiveMinimum as a boolean)

JSON Schema draft 2020-12 in full (type: [string, "null"], numeric exclusiveMinimum, $ref with sibling keywords)

Webhooks

not available

top-level webhooks

Root requirements

paths required

one of paths, components or webhooks

The OpenAPI Initiative has published later versions as well (the specification site lists 3.2.1, dated 2026-09-10, as the latest). The Flask libraries on this page generate documents that follow the 3.0 and 3.1 shapes: APIFlask defaults to 3.1.0 and flask-openapi3 uses 3.1.0, while flask-smorest has no default and requires you to set OPENAPI_VERSION (its quickstart uses 3.0.2). apispec, the library underneath flask-smorest, accepts any version string without checking it, so the value you choose is a promise you must keep: set 3.1.0 and let tools validate the result. The book Microservice APIs configures 3.0.3; 3.1 is the better target for new work because its schemas are plain JSON Schema, which every modern validator, test generator and code generator understands.

The Flask Libraries

Table 1. Libraries that give Flask an OpenAPI document (versions checked 2026-10-07)
Library Validation and schemas OpenAPI output Notes

flask-smorest 0.47.0

marshmallow (through webargs), apispec

the version you set in OPENAPI_VERSION

plain Flask Blueprint and MethodView; the subject of most of this page

APIFlask 3.1.2

marshmallow (apispec underneath)

3.1.0 by default

a Flask subclass with @app.input, @app.output, @app.doc; see APIFlask: the Same Idea in a Flask Subclass

flask-openapi3 4.3.2

Pydantic 2

3.1.0

OpenAPI and APIBlueprint classes; models in type annotations; UIs as optional extras

Flask-RESTX 1.3.2

its own fields and reqparse

Swagger 2.0 ("swagger": "2.0")

community fork of Flask-RESTPlus; the output predates OpenAPI 3

Connexion

the OpenAPI document itself

the document you wrote

spec-first framework that routes operationId values to Python functions and validates requests and responses

Flask-RESTful

reqparse (deprecated)

none

code you will meet in older books; no OpenAPI support

Flask-RESTful with reqparse, used by Flask Framework Cookbook (Packt), is maintained at a minimal level and has no OpenAPI support; use one of the libraries above for new work. A fuller comparison with maintenance status belongs to the page on building REST APIs in this section.

Code-First with flask-smorest

flask-smorest (installed with pip install flask-smorest) combines three libraries: marshmallow describes the data, webargs parses requests into those schemas, and apispec builds the OpenAPI document from everything it sees. The Flask part is deliberately small: an Api object attached to the application, a Blueprint subclass whose decorators carry the documentation, and the standard Flask MethodView from Class-Based Views to group the methods of a resource.

The Minimal API

Three settings are required, and Api raises MissingAPIParameterError when one is absent: API_TITLE, API_VERSION and OPENAPI_VERSION. Everything else is optional.

from flask import Flask
from flask.views import MethodView
import marshmallow as ma
from flask_smorest import Api, Blueprint, abort


class BookSchema(ma.Schema):
    id = ma.fields.Int(dump_only=True)
    title = ma.fields.Str(required=True, validate=ma.validate.Length(min=1, max=200))
    year = ma.fields.Int(validate=ma.validate.Range(min=1450))


blp = Blueprint("books", __name__, url_prefix="/books", description="Operations on books")
BOOKS: dict[int, dict] = {}


@blp.route("/")
class Books(MethodView):
    @blp.response(200, BookSchema(many=True))
    def get(self):
        """List the books"""
        return list(BOOKS.values())

    @blp.arguments(BookSchema)
    @blp.response(201, BookSchema)
    def post(self, data):
        """Create a book"""
        data["id"] = len(BOOKS) + 1
        BOOKS[data["id"]] = data
        return data


@blp.route("/<int:book_id>")
class Book(MethodView):
    @blp.response(200, BookSchema)
    def get(self, book_id):
        """Fetch one book"""
        try:
            return BOOKS[book_id]
        except KeyError:
            abort(404, message="No such book")


def create_app() -> Flask:
    app = Flask(__name__)
    app.config.update(
        API_TITLE="Bookshelf API",
        API_VERSION="v1",
        OPENAPI_VERSION="3.1.0",
    )
    api = Api(app)
    api.register_blueprint(blp)
    return app


app = create_app()
client = app.test_client()
print(client.post("/books/", json={"title": "Dune", "year": 1965}).status_code)
print(client.get("/books/1").json)
print(client.get("/books/9").json)
print(client.post("/books/", json={"year": 1965}).json)

With the development server (flask --app bookshelf run) the same calls look like this, and the last one shows the error format that flask-smorest installs for every HTTPException.

curl -s -X POST localhost:5000/books/ -H 'Content-Type: application/json' -d '{"title": "Dune", "year": 1965}'
curl -s -X POST localhost:5000/books/ -H 'Content-Type: application/json' -d '{"year": 1965}'
{"id": 1, "title": "Dune", "year": 1965}
{"code": 422, "errors": {"json": {"title": ["Missing data for required field."]}}, "status": "Unprocessable Entity"}

What happened underneath:

  • Api(app) read the three settings, built an apispec.APISpec, registered a default error handler for HTTPException and added the flask openapi command group to app.cli.

  • Blueprint is a subclass of flask.Blueprint; blp.route registers the view like Flask does and also stores the view in the documentation. Because it is a real Flask blueprint, everything in Blueprints (prefixes, error handlers, hooks) still applies.

  • api.register_blueprint(blp) registers it in the application and generates the documentation of its views from the decorators and docstrings. It must be called after Api is attached to the app; use Api().init_app(app) in an application factory, as in Application Factory & Project Layout.

  • A blueprint registered with plain app.register_blueprint(blp) works as a Flask blueprint but never appears in the document.

The Decorators and Their Order

The decorators belong to the Blueprint; each one both changes the runtime behaviour of the view and contributes to the document.

Decorator Runtime effect Documentation effect

@blp.route(rule, parameters=None, tags=None, **options)

registers the view (a function or a MethodView class) under rule

creates the path item; parameters documents path parameters declared in rule or in url_prefix

@blp.arguments(schema, location="json", required=True, description=None, example=None, examples=None, content_type=None)

parses and validates the request with the schema (through webargs) and passes the result as an extra positional argument; on failure answers 422 with the errors

requestBody for json, form and files; for any other location one entry per schema field in parameters

@blp.response(status_code, schema=None, content_type=None, description=None, example=None, examples=None, headers=None)

serializes the returned object with the schema; the status code is used unless the view returns a tuple with another

the success response, with the schema referenced from components.schemas

@blp.alt_response(status_code, response=None, schema=None, description=None, example=None, examples=None, headers=None, success=False)

nothing by default: it only documents a response the view may produce

an additional response (typically an error) for the operation; the parameter success=True marks it as part of the normal flow so pagination and ETag also apply to it

@blp.paginate(pager=None, page=None, page_size=None, max_page_size=None)

adds page and page_size query parameters and the X-Pagination header

documents the two parameters and the header

@blp.doc(**kwargs)

nothing

merges any keyword (summary, description, security, deprecated, operationId, tags, responses…​) into the operation

Three rules about the order, all of which the book Microservice APIs gets wrong or leaves out, decide whether the document and the behaviour are correct.

  1. @blp.route must be the outermost decorator when decorating a function. It registers whatever function it receives, so a @blp.response written above it is applied to a function that is no longer the one Flask calls. With a MethodView class the problem does not exist, because @blp.route decorates the class and the other decorators sit on the methods.

  2. Under @blp.route, put @blp.arguments first (outermost) and @blp.response below it; @blp.paginate goes below @blp.response. The view’s own signature mirrors that order: the parsed arguments come first, then pagination_parameters when pagination is handled in the view.

  3. The first positional argument of @blp.response is the status code and the schema is the second. Writing @blp.response(BookSchema) fails at import time with a TypeError, because a schema class is not a status code. The status_code-first signature has existed since flask-smorest 0.29.0 (2021-02-13); older tutorials and the book use the previous form, with the schema first.

flowchart TD R["HTTP request"] --> RT["Flask routing
blp.route registered the view"] RT --> ARG["blp.arguments
webargs parses and validates
422 on failure"] ARG --> PG["blp.response and blp.paginate
wrap the call"] PG --> V["your view function
receives the parsed data"] V --> SER["blp.response
serializes with the schema
unknown attributes are dropped"] SER --> HDR["blp.paginate
adds X-Pagination"] HDR --> OUT["HTTP response"] ARG -. "documents requestBody or parameters" .-> SPEC["OpenAPI document"] SER -. "documents responses" .-> SPEC
Figure 2. How a request travels through the stacked decorators

The wrong order is not just a documentation bug, it is a data leak. The view below returns an attribute that is not in the schema; with the right order the response schema filters it out, with the wrong order the decorator is bypassed and the attribute is sent (and the success response is missing from the document).

import marshmallow as ma
from flask import Flask
from flask_smorest import Api, Blueprint


class PublicBook(ma.Schema):
    title = ma.fields.Str()


app = Flask(__name__)
app.config.update(API_TITLE="Order demo", API_VERSION="v1", OPENAPI_VERSION="3.1.0")
api = Api(app)
blp = Blueprint("order", __name__, url_prefix="/order")


@blp.response(200, PublicBook)
@blp.route("/wrong")
def wrong():
    return {"title": "Dune", "internal_note": "do not publish"}


@blp.route("/right")
@blp.response(200, PublicBook)
def right():
    return {"title": "Dune", "internal_note": "do not publish"}


api.register_blueprint(blp)
c = app.test_client()
print(c.get("/order/wrong").json)
print(c.get("/order/right").json)
spec = api.spec.to_dict()
print("200 documented for wrong:", "200" in spec["paths"]["/order/wrong"]["get"]["responses"])
print("200 documented for right:", "200" in spec["paths"]["/order/right"]["get"]["responses"])

Configuration Reference

flask-smorest reads its settings from app.config. All keys can carry a prefix when several Api instances live in one application: pass config_prefix="V2_" to Api and write V2_API_TITLE and so on.

Key Default Meaning

API_TITLE

required

the human-friendly title (info.title)

API_VERSION

required

the version of the API as a string (info.version)

OPENAPI_VERSION

required

the specification version string written to openapi, for example 3.1.0

OPENAPI_URL_PREFIX

None

base path under which the JSON document and the UIs are served; None disables serving altogether

OPENAPI_JSON_PATH

openapi.json

path of the JSON document relative to the prefix

OPENAPI_SWAGGER_UI_PATH, OPENAPI_SWAGGER_UI_URL

None

path of the Swagger UI page and the base URL of the Swagger UI scripts (version 3 or later); both must be set

OPENAPI_SWAGGER_UI_CONFIG

{}

dictionary of Swagger UI configuration options

OPENAPI_REDOC_PATH, OPENAPI_REDOC_URL

None

path of the ReDoc page and URL of the ReDoc script; both must be set

OPENAPI_RAPIDOC_PATH, OPENAPI_RAPIDOC_URL

None

path of the RapiDoc page and URL of its script; both must be set

OPENAPI_RAPIDOC_CONFIG

{}

dictionary of RapiDoc options

API_SPEC_OPTIONS

{}

extra root attributes of the document (servers, info additions, security…​), passed to apispec

Api also accepts spec_kwargs (passed to apispec.APISpec, including flask_plugin, marshmallow_plugin and extra_plugins) and Api.register_field and Api.register_converter to teach the generator how to document custom marshmallow fields and custom URL converters.

Describing Data with marshmallow Schemas

A marshmallow Schema does three jobs at once: it validates and deserializes input (load), it serializes output (dump), and, through apispec, it becomes an entry under components.schemas of the document. The vocabulary that matters for an API is small.

Feature Effect on behaviour and on the document

fields.Str(), Int(), Float(), Bool(), Date(), DateTime(), UUID(), List(), Dict()

types of the properties; DateTime becomes format: date-time, UUID becomes format: uuid

required=True

input without the field is rejected; the field is listed under required

dump_only=True / load_only=True

read-only fields (id, timestamps) are ignored on input and marked readOnly; write-only fields (passwords) never appear in output and are marked writeOnly

load_default= / dump_default=

value used when the field is missing on input / output; in marshmallow 3.13 and later these replace the old missing= and default= keywords, and load_default=list is the way to default to a fresh list

validate= (Length, Range, OneOf, Regexp, custom callables)

rejects the input with a 422; most standard validators become minLength, maxLength, minimum, maximum, enum and pattern

@validates("field"), @validates_schema

custom field-level and cross-field validation; custom validators are not visible in the document, so describe them in prose

fields.Nested(OtherSchema)

a nested object, rendered as a $ref to the other schema; many=True makes an array

Meta.unknown

what to do with fields the input contains but the schema does not declare; see below

Meta.exclude, only=, partial=

subset or partial variants, which become separate component names, so use them sparingly

schema inheritance

a subclass inherits the parent’s fields; a subclass is a distinct component, so put the common fields in a base class and keep Meta options in it

The Unknown-Field Policy

Meta.unknown takes one of three values, and the two that matter are the opposite of what Microservice APIs states.

Value Behaviour on load Effect on the document

RAISE (the default)

unknown fields are rejected: the request fails with a 422 and Unknown field. for each one

additionalProperties: false

EXCLUDE

unknown fields are silently dropped: the request succeeds and the view never sees them

no additionalProperties keyword

INCLUDE

unknown fields are kept in the result

no additionalProperties keyword

EXCLUDE therefore means tolerant (drop what you do not know), and RAISE strict. The choice is a design decision: strict input catches misspelt optional fields (a client that sends titel hears about it), tolerant input lets old servers accept newer clients. Note that the default for output schemas is irrelevant at runtime, because dumping ignores attributes the schema does not declare, as the leak demonstration above showed.

import json
import marshmallow as ma


class Base(ma.Schema):
    """Common configuration for every schema of the API."""

    class Meta:
        unknown = ma.RAISE


class AuthorSchema(Base):
    name = ma.fields.Str(required=True)


class LenientAuthor(AuthorSchema):
    class Meta:
        unknown = ma.EXCLUDE


class BookInSchema(Base):
    title = ma.fields.Str(required=True)
    authors = ma.fields.List(ma.fields.Nested(LenientAuthor), load_default=list)

    @ma.validates("title")
    def no_padding(self, value, **kwargs):
        if value != value.strip():
            raise ma.ValidationError("must not start or end with spaces")


try:
    BookInSchema().load({"title": "Dune", "isbn": "x"})
except ma.ValidationError as err:
    print("RAISE:", err.messages)
print("EXCLUDE:", BookInSchema().load({"title": "Dune", "authors": [{"name": "Herbert", "born": 1920}]}))
try:
    BookInSchema().load({"title": " Dune"})
except ma.ValidationError as err:
    print("validator:", err.messages)

# What the policy does to the generated document
from flask import Flask
from flask.views import MethodView
from flask_smorest import Api, Blueprint

app = Flask(__name__)
app.config.update(API_TITLE="Policy demo", API_VERSION="v1", OPENAPI_VERSION="3.1.0")
api = Api(app)
blp = Blueprint("policy", __name__, url_prefix="/policy")


@blp.route("/")
class Policy(MethodView):
    @blp.arguments(BookInSchema)
    @blp.response(200, BookInSchema)
    def post(self, data):
        return data


api.register_blueprint(blp)
schemas = api.spec.to_dict()["components"]["schemas"]
print(json.dumps({k: schemas[k].get("additionalProperties") for k in ("BookIn", "LenientAuthor")}))

Schema names in components.schemas come from the class name with a trailing Schema removed (BookInSchema becomes BookIn); two different schema classes that end up with the same component name trigger an apispec warning, so keep names distinct across blueprints.

Parsing Query Strings, Forms and Headers

@blp.arguments(schema, location="json") is not limited to bodies. The location argument chooses where webargs looks for the data: json (the default), query, form, files, headers, cookies, among others webargs supports. For json, form and files the schema becomes the requestBody and required and description apply to it; for the other locations each field of the schema becomes one entry of parameters with its own required flag, and the required argument of the decorator has no effect. That is the part worth remembering when a query parameter looks optional in the document although you passed required=True.

Query schemas usually want unknown = EXCLUDE, because browsers, proxies and trackers add parameters (utm_source, _ cache busters) that you do not want to turn into a 422.

import marshmallow as ma
from flask import Flask
from flask.views import MethodView
from flask_smorest import Api, Blueprint


class BookQuery(ma.Schema):
    class Meta:
        unknown = ma.EXCLUDE

    q = ma.fields.Str(metadata={"description": "Title contains this text"})
    year_from = ma.fields.Int(validate=ma.validate.Range(min=1450))
    tags = ma.fields.List(ma.fields.Str())


app = Flask(__name__)
app.config.update(API_TITLE="Query demo", API_VERSION="v1", OPENAPI_VERSION="3.1.0")
api = Api(app)
blp = Blueprint("search", __name__, url_prefix="/search")


@blp.route("/")
class Search(MethodView):
    @blp.arguments(BookQuery, location="query")
    @blp.response(200)
    def get(self, args):
        return args


api.register_blueprint(blp)
c = app.test_client()
print(c.get("/search/?q=dune&utm_source=mail&tags=a&tags=b").json)
print(c.get("/search/?year_from=10").json)
params = api.spec.to_dict()["paths"]["/search/"]["get"]["parameters"]
print([(p["name"], p["in"], p["required"]) for p in params])

Responses, Errors and Status Codes

@blp.response(status_code, schema) describes the success response. A few points about it are easy to miss.

  • If a view returns a list, the schema must be instantiated with many=True (BookSchema(many=True)), otherwise the list is serialized as if it were a single object.

  • The decorator serializes with the schema (dump) but does not validate the data: a view that returns {"title": 5} for a Str field sends {"title": "5"} with status 200. Contract problems in outgoing data are caught by tests that compare responses to the document (see Design-First with a Hand-Written Document), not by the decorator.

  • A view may return a tuple, such as (body, 201, headers). A status code in the tuple overrides the one in the decorator, and the document keeps showing the decorator’s value, which is why the documentation of flask-smorest advises against it.

  • A single @blp.response is allowed per view. Everything else the view may answer is documented with stacked @blp.alt_response decorators. With only a status code, alt_response documents that status with its standard HTTP phrase as description; with response="NOT_FOUND" (the name of an http.HTTPStatus member) it references the shared component #/components/responses/NOT_FOUND, created on demand with the error schema, so every operation that uses it shares one definition.

Errors have a default format. abort(status, message=..., errors=..., headers=...) is a republication of webargs' abort that stores its keyword arguments so the registered handler can render them as {"code": ..., "status": ..., "message": ..., "errors": ...}. Validation failures raised by @blp.arguments are the same kind of HTTPException, with the field errors under errors keyed by location. The default answer is documented once as components.schemas.Error and used for every operation as the default response, plus a 422 response for operations with @blp.arguments.

import marshmallow as ma
from flask import Flask
from flask.views import MethodView
from flask_smorest import Api, Blueprint, abort


class ShelfItem(ma.Schema):
    isbn = ma.fields.Str(required=True)


app = Flask(__name__)
app.config.update(API_TITLE="Errors demo", API_VERSION="v1", OPENAPI_VERSION="3.1.0")
api = Api(app)
blp = Blueprint("shelf", __name__, url_prefix="/shelf")
SHELF: set[str] = set()


@blp.route("/")
class Shelf(MethodView):
    @blp.arguments(ShelfItem)
    @blp.response(201, ShelfItem)
    @blp.alt_response(409, description="The book is already on the shelf")
    def post(self, data):
        if data["isbn"] in SHELF:
            abort(409, message="Already on the shelf", errors={"isbn": ["duplicate"]})
        SHELF.add(data["isbn"])
        return data

    @blp.response(200)
    def get(self):
        raise ma.ValidationError("a schema error raised by hand")


api.register_blueprint(blp)
app.config["PROPAGATE_EXCEPTIONS"] = False
c = app.test_client()
print(c.post("/shelf/", json={"isbn": "978-0"}).status_code)
print(c.post("/shelf/", json={"isbn": "978-0"}).json)
print(c.post("/shelf/", json={}).json)
print("hand-raised ValidationError ->", c.get("/shelf/").status_code)
print(sorted(api.spec.to_dict()["paths"]["/shelf/"]["post"]["responses"]))

The last two lines show a pitfall: marshmallow’s ValidationError raised inside a view is not an HTTPException, so it is not converted to a 422; Flask turns it into a plain 500 (see Error Handling for how the handlers are chosen). Validation belongs in @blp.arguments; to fail from the middle of a view use abort(422, ...) or abort(409, ...), or register your own handler for the exception.

Problem Details Instead of the Default Format

RFC 9457 (Problem Details for HTTP APIs, which obsoletes RFC 7807) defines the application/problem+json format that many clients and gateways understand; the books on this page predate it or ignore it. The default flask-smorest format is a custom shape, but the error handler and the error schema are both attributes of Api and can be replaced by subclassing: ERROR_SCHEMA is the schema documented in the spec and handle_http_exception builds the response.

import marshmallow as ma
from flask import Flask, make_response
from flask.views import MethodView
from flask_smorest import Api, Blueprint, abort


class Problem(ma.Schema):
    type = ma.fields.Str(dump_default="about:blank")
    title = ma.fields.Str()
    status = ma.fields.Int()
    detail = ma.fields.Str()
    errors = ma.fields.Dict()


class ProblemApi(Api):
    ERROR_SCHEMA = Problem

    def handle_http_exception(self, error):
        payload, status, headers = super().handle_http_exception(error)
        body = {"title": payload["status"], "status": status}
        if "message" in payload:
            body["detail"] = payload["message"]
        if "errors" in payload:
            body["errors"] = payload["errors"]
        response = make_response(Problem().dumps(body), status)
        response.headers["Content-Type"] = "application/problem+json"
        response.headers.update(headers)
        return response


class Note(ma.Schema):
    n = ma.fields.Int(required=True)


app = Flask(__name__)
app.config.update(API_TITLE="Problem demo", API_VERSION="v1", OPENAPI_VERSION="3.1.0")
api = ProblemApi(app)
blp = Blueprint("notes", __name__, url_prefix="/notes")


@blp.route("/")
class Notes(MethodView):
    @blp.arguments(Note)
    @blp.response(200, Note)
    def post(self, data):
        abort(409, message="Conflict here")


api.register_blueprint(blp)
c = app.test_client()
r = c.post("/notes/", json={"n": "x"})
print(r.status_code, r.content_type, r.json)
r = c.post("/notes/", json={"n": 1})
print(r.status_code, r.content_type, r.json["detail"])
print("Error component replaced:", "Problem" in api.spec.to_dict()["components"]["schemas"])

The documented error responses still advertise application/json as their media type, because the default response content type of the spec generator is a single class attribute used for success and error responses alike; check the generated document if clients rely on that media type. The page on error handling in this section shows the same RFC 9457 format for plain Flask.

Pagination

@blp.paginate() adds page and page_size to the query parameters and the metadata to the response in an X-Pagination header (total, total_pages, page, first_page, last_page, and previous_page and next_page when they exist). It has two modes. In the first the view receives a pagination_parameters object, slices its own data and sets item_count. In the second paginate receives a pager class (a subclass of flask_smorest.Page, such as one wrapping a SQLAlchemy query) and the view just returns the whole collection or a lazy cursor.

Setting Default

page

1

page_size

10

max_page_size

100

They can be changed globally in Blueprint.DEFAULT_PAGINATION_PARAMETERS or per endpoint as arguments of @blp.paginate(page=..., page_size=..., max_page_size=...); the header name is Blueprint.PAGINATION_HEADER_NAME (None disables the header). A request for a larger page_size than allowed is rejected by the generated parameter schema (maximum: 100), which protects the database from page_size=1000000.

import marshmallow as ma
from flask import Flask
from flask.views import MethodView
from flask_smorest import Api, Blueprint, Page


class ItemSchema(ma.Schema):
    n = ma.fields.Int()


app = Flask(__name__)
app.config.update(API_TITLE="Paging demo", API_VERSION="v1", OPENAPI_VERSION="3.1.0")
api = Api(app)
blp = Blueprint("items", __name__, url_prefix="/items")
ITEMS = [{"n": i} for i in range(1, 26)]


class ListPage(Page):
    @property
    def item_count(self):
        return len(self.collection)


@blp.route("/in-view")
class InView(MethodView):
    @blp.response(200, ItemSchema(many=True))
    @blp.paginate()
    def get(self, pagination_parameters):
        pagination_parameters.item_count = len(ITEMS)
        return ITEMS[pagination_parameters.first_item : pagination_parameters.last_item + 1]


@blp.route("/with-pager")
class WithPager(MethodView):
    @blp.response(200, ItemSchema(many=True))
    @blp.paginate(ListPage, page_size=5, max_page_size=20)
    def get(self):
        return ITEMS


api.register_blueprint(blp)
c = app.test_client()
r = c.get("/items/in-view?page=3&page_size=10")
print(len(r.json), r.headers["X-Pagination"])
r = c.get("/items/with-pager?page=2")
print([x["n"] for x in r.json], r.headers["X-Pagination"])
print(c.get("/items/with-pager?page_size=500").status_code)

The related ETag support (@blp.etag, conditional If-None-Match and If-Match requests for caching and optimistic locking) is described in the flask-smorest documentation and adds its own documented headers; the page on building REST APIs in this section covers when to use pagination styles and conditional requests.

Enriching the Generated Document

The decorators produce structure; people read the prose. Four sources add it.

  • Docstrings. The first line of a view’s docstring becomes the operation summary and the rest, until a line containing only ---, the description. Anything after the --- is not published, which makes the delimiter a place for internal notes.

  • @blp.doc(...) merges arbitrary keywords into the operation: summary, description, tags, deprecated=True, operationId, security=[...], externalDocs, servers.

  • Field metadata: ma.fields.Str(metadata={"description": "...", "example": "..."}) documents a property.

  • API_SPEC_OPTIONS and api.spec.components: root attributes (servers, info.description, info.contact…​) and reusable components registered with apispec’s Components API (security_scheme, parameter, response…​).

Security deserves its own mention. The document is where an operation says "this requires a bearer token", but flask-smorest only describes security, it does not enforce it: the view or a decorator from Hooks & View Decorators still has to authenticate the request, as shown in Authorization & Roles. A securitySchemes entry plus a security requirement makes Swagger UI and generated clients send the credential. The page on JWT and API authentication in this section builds the matching implementation.

import json
import marshmallow as ma
from flask import Flask
from flask.views import MethodView
from flask_smorest import Api, Blueprint, abort


class BookOut(ma.Schema):
    title = ma.fields.Str(metadata={"description": "Title as printed on the cover", "example": "Dune"})


app = Flask(__name__)
app.config.update(
    API_TITLE="Bookshelf API",
    API_VERSION="v1",
    OPENAPI_VERSION="3.1.0",
    API_SPEC_OPTIONS={
        "servers": [{"url": "https://api.example.com", "description": "Production"}],
        "info": {"description": "The Bookshelf catalogue.", "contact": {"name": "API team"}},
    },
)
api = Api(app)
api.spec.components.security_scheme(
    "bearerAuth", {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}
)
blp = Blueprint("catalog", __name__, url_prefix="/catalog", description="The catalogue of books")


@blp.route("/<int:book_id>")
class CatalogBook(MethodView):
    @blp.doc(security=[{"bearerAuth": []}], operationId="getCatalogBook")
    @blp.response(200, BookOut)
    @blp.alt_response(404, description="No book with that id")
    def get(self, book_id):
        """Fetch a book

        Returns the book with the given identifier.
        ---
        Internal: backed by the read replica, see the runbook.
        """
        abort(404)


@blp.route("/legacy")
class Legacy(MethodView):
    @blp.doc(deprecated=True)
    @blp.response(200)
    def get(self):
        """Old listing, use /catalog/ instead"""
        return []


api.register_blueprint(blp)
spec = api.spec.to_dict()
op = spec["paths"]["/catalog/{book_id}"]["get"]
print(op["summary"], "|", op["description"], "|", op["operationId"], op["security"])
print(spec["servers"], spec["components"]["securitySchemes"])
print(spec["paths"]["/catalog/legacy"]["get"]["deprecated"], spec["tags"])
print(json.dumps(spec["components"]["schemas"]["BookOut"]["properties"]))

Serving the Document and the Interactive UIs

When OPENAPI_URL_PREFIX is set, flask-smorest registers an extra blueprint that serves the document at <prefix>/openapi.json (or OPENAPI_JSON_PATH) and, for each UI whose path and script URL are both set, an HTML page that loads the UI from that URL. The three supported UIs are Swagger UI, ReDoc and RapiDoc; none is bundled, they are pulled from a CDN or from a location you host.

from flask import Flask
from flask_smorest import Api

DOCS = {
    "API_TITLE": "Bookshelf API",
    "API_VERSION": "v1",
    "OPENAPI_VERSION": "3.1.0",
    "OPENAPI_URL_PREFIX": "/docs",
    "OPENAPI_SWAGGER_UI_PATH": "/swagger",
    "OPENAPI_SWAGGER_UI_URL": "https://cdn.jsdelivr.net/npm/swagger-ui-dist/",
    "OPENAPI_SWAGGER_UI_CONFIG": {"persistAuthorization": True},
    "OPENAPI_REDOC_PATH": "/redoc",
    "OPENAPI_REDOC_URL": "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js",
    "OPENAPI_RAPIDOC_PATH": "/rapidoc",
    "OPENAPI_RAPIDOC_URL": "https://unpkg.com/rapidoc/dist/rapidoc-min.js",
}

app = Flask(__name__)
app.config.update(DOCS)
Api(app)
c = app.test_client()
for path in ("/docs/openapi.json", "/docs/swagger", "/docs/redoc", "/docs/rapidoc"):
    print(path, c.get(path).status_code)
print([r.endpoint for r in app.url_map.iter_rules() if r.endpoint.startswith("api-docs")])

Publishing documentation raises operational questions that a decorator cannot answer.

  • Third-party scripts. The pages above execute JavaScript from cdn.jsdelivr.net or unpkg.com, unpinned in the example (swagger-ui-dist with no version resolves to the latest release). That is convenient in development. In production pin an exact version in the URL, or download the distribution into the application’s static folder (see Static Files & Assets) and point the URL setting at it; this also keeps a strict Content-Security-Policy workable, as in Security Considerations.

  • Exposure. The document and the UI list every endpoint, including ones meant for internal clients. Decide whether they are public. A common pattern is to set OPENAPI_URL_PREFIX only when DEBUG is on or from an environment variable, so production serves nothing (None disables the whole blueprint), or to put the prefix behind the same authentication as the API.

  • Try it out. Swagger UI sends real requests with real credentials from the visitor’s browser. Point its servers entry at the right environment, and keep the CORS policy of the API consistent with where the UI is hosted; the page on single-page apps and CORS in this section and the site’s own web/cors page explain preflight requests.

  • Hosting the files elsewhere. The document is just a file: publishing the output of flask openapi write to a documentation site or an API portal avoids exposing the live service at all.

The OpenAPI Command-Line Group

Api registers a Click group named openapi (see the page on the Flask CLI). flask --app bookshelf openapi print writes the document to standard output and flask --app bookshelf openapi write openapi.json writes it to a file; both accept --format json|yaml (YAML needs PyYAML installed) and --config-prefix for applications with several Api objects; openapi list-config-prefixes shows the prefixes.

flask --app bookshelf openapi print --format yaml
flask --app bookshelf openapi write openapi.json
git diff --exit-code openapi.json   # fail the CI job when the committed spec is stale

Committing the generated file and failing the build when git diff is not empty turns every API change into a visible diff in the pull request, which is how code-first projects get the review step that design-first projects get from writing the document by hand. The same generated file is the input to linters, generators and contract tests, so no server has to be running.

import json
import tempfile
from pathlib import Path

from openapi_spec_validator import validate

app = create_app()  # the factory defined in the minimal API example
runner = app.test_cli_runner()
with tempfile.TemporaryDirectory() as tmp:
    target = Path(tmp) / "openapi.json"
    result = runner.invoke(args=["openapi", "write", str(target)])
    print("exit code:", result.exit_code)
    document = json.loads(target.read_text())
validate(document)  # raises on an invalid OpenAPI document
print(document["openapi"], sorted(document["paths"]))

APIFlask: the Same Idea in a Flask Subclass

APIFlask (pip install apiflask) takes a different shape. APIFlask subclasses Flask, so plain Flask views keep working, and it adds decorators on the application and its blueprints (APIBlueprint) instead of on a separate Blueprint class. It documents and serves the OpenAPI document and a UI out of the box with no extra configuration, and it accepts marshmallow schemas.

Concern flask-smorest APIFlask

Application object

Flask plus Api(app)

APIFlask(__name__, title=..., version=...)

Request body / query

@blp.arguments(Schema, location=...); the parsed data is a positional argument

@app.input(Schema, location="json"); the data is passed as json_data (and query_data, form_data…​)

Success response

@blp.response(status, Schema)

@app.output(Schema, status_code=200)

Other responses

@blp.alt_response

@app.doc(responses=[404, 409])

Extra documentation

@blp.doc(...), docstring with ---

@app.doc(summary=..., description=..., tags=..., deprecated=..., hide=...)

Automatic responses

422 for arguments and a default error response

200; 400, 404 and 401 added when input, a path variable or authentication are present (configurable with the AUTO_* settings)

Default document version

set OPENAPI_VERSION yourself

3.1.0 (OPENAPI_VERSION)

UI

Swagger UI, ReDoc, RapiDoc (URLs set by you)

Swagger UI at /docs by default; docs_ui can be redoc, elements, rapidoc or rapipdf; docs_path=None disables it

CLI

flask openapi print and flask openapi write

flask spec with --output, --format (json, yaml, yml), --indent and --quiet

Spec file in the repository

redirect openapi write to a file

LOCAL_SPEC_PATH plus SYNC_LOCAL_SPEC

from apiflask import APIFlask, Schema, abort
from apiflask.fields import Integer, String
from apiflask.validators import Length, Range

app = APIFlask(__name__, title="Bookshelf API", version="1.0")


class BookIn(Schema):
    title = String(required=True, validate=Length(1, 200))
    year = Integer(validate=Range(min=1450))


class BookOut(BookIn):
    id = Integer()


BOOKS: dict[int, dict] = {}


@app.post("/books")
@app.input(BookIn)
@app.output(BookOut, status_code=201)
@app.doc(summary="Create a book", responses=[409])
def create_book(json_data):
    json_data["id"] = len(BOOKS) + 1
    BOOKS[json_data["id"]] = json_data
    return json_data


@app.get("/books/<int:book_id>")
@app.output(BookOut)
def get_book(book_id):
    if book_id not in BOOKS:
        abort(404, "No such book")
    return BOOKS[book_id]


c = app.test_client()
print(c.post("/books", json={"title": "Dune"}).json)
print(c.post("/books", json={"title": ""}).json)
print(c.get("/books/9").json)
document = c.get("/openapi.json").json
print(document["openapi"], sorted(document["paths"]), sorted(document["components"]["schemas"]))

In the command line, flask --app bookshelf spec --format yaml --output openapi.yaml produces the same kind of file as the flask-smorest command. The flask spec command is installed as a Flask plugin command (a flask.commands entry point), so it appears in the real flask executable but not in app.test_cli_runner(). APIFlask’s @app.spec_processor hook lets you rewrite the finished document, and @app.doc(hide=True) or APIFlask(__name__, enable_openapi=False) removes operations or the whole feature.

Design-First with a Hand-Written Document

In design-first the document is the product of a design conversation. The team writes the YAML (an editor with an OpenAPI plug-in, or a design tool), reviews it, publishes it to front-end and partner teams, and only then implements. The risk is drift, so the workflow needs automation at three points: the document must be valid, the application must serve or reference it, and the implementation must be checked against it.

Validate the document. openapi-spec-validator (pip install openapi-spec-validator) checks a document against the specification schema for the version it declares, including $ref resolution, and raises on the first problem.

Serve it. Nothing in Flask requires a framework: a view that returns the parsed document is enough, and any of the UIs from the previous section can be pointed at it. With flask-smorest, set OPENAPI_URL_PREFIX to None (so the generated document is not published) and serve your file from your own route. The book Microservice APIs replaces the served document by monkeypatching APISpec.to_dict; that works only as a workaround, because it ties the application to a private detail of the library and defeats the point of the generator. If you want the spec to drive the application rather than sit beside it, Connexion maps each operationId to a Python function and validates requests and responses against the document at run time.

Check the implementation. The simplest contract test needs only jsonschema: for every operation, call the endpoint with the Flask test client and validate the response body against the schema the document declares for that status code. Components are referenced as #/components/schemas/..., so the validator is given a root object that carries both the schema and components. Dedicated tools do the same at scale: Schemathesis generates the requests from the document and fuzzes the API, and openapi-core validates requests and responses with Flask integration (both discussed in the page on API contract and property-based testing in this section; Dredd, used in the book, is archived).

import yaml
from flask import Flask, jsonify
from jsonschema import Draft202012Validator
from openapi_spec_validator import validate

SPEC = yaml.safe_load(
    """
openapi: 3.1.0
info: {title: Bookshelf API, version: 1.0.0}
paths:
  /books/{bookId}:
    get:
      operationId: getBook
      parameters:
        - {name: bookId, in: path, required: true, schema: {type: integer, minimum: 1}}
      responses:
        "200":
          description: The book
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Book"}
        "404":
          description: Not found
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Problem"}
components:
  schemas:
    Book:
      type: object
      required: [id, title]
      additionalProperties: false
      properties:
        id: {type: integer}
        title: {type: string, minLength: 1}
        subtitle: {type: [string, "null"]}
    Problem:
      type: object
      required: [status, title]
      properties:
        status: {type: integer}
        title: {type: string}
"""
)
validate(SPEC)  # raises if the document itself is invalid

app = Flask(__name__)


@app.get("/openapi.json")
def serve_spec():
    return jsonify(SPEC)


@app.get("/books/<int:book_id>")
def get_book(book_id):
    if book_id != 1:
        return jsonify(status=404, title="Not Found"), 404
    return jsonify(id=1, title="Dune", subtitle=None)


def assert_conforms(path, method, status, body):
    """Validate a response body against the schema the document declares."""
    response_doc = SPEC["paths"][path][method]["responses"][str(status)]
    schema = response_doc["content"]["application/json"]["schema"]
    root = {**schema, "components": SPEC["components"]}
    problems = [e.message for e in Draft202012Validator(root).iter_errors(body)]
    assert not problems, problems


client = app.test_client()
for book_id, expected in ((1, 200), (2, 404)):
    response = client.get(f"/books/{book_id}")
    assert response.status_code == expected
    assert_conforms("/books/{bookId}", "get", expected, response.json)
print("implementation conforms")
try:
    assert_conforms("/books/{bookId}", "get", 200, {"id": 1, "title": "", "isbn": "x"})
except AssertionError as err:
    print("drift detected:", err)

Generating Clients from the Document

Because the document is the contract, consumers can generate their side of it. Quality in equals quality out: give every operation a stable operationId (flask-smorest does not invent one, so set it with @blp.doc(operationId=...); APIFlask has the AUTO_OPERATION_ID setting) because generators use it as the method name, and give every schema a meaningful name because they use it as the class name.

# Python client, typed, from a file or a URL (OpenAPI 3.0 and 3.1)
pipx install openapi-python-client --include-deps
openapi-python-client generate --path openapi.json

# Many languages through the Java-based OpenAPI Generator
npm install @openapitools/openapi-generator-cli -g
openapi-generator-cli generate -i openapi.json -g typescript-fetch -o ./client

Review generated clients like any dependency: regenerate them in CI from the committed document so that a breaking API change fails somewhere visible, and treat the generated code as build output, not as hand-edited source.

Pitfalls and Security Notes

Pitfall What to do

@blp.response above @blp.route

the response schema is bypassed (unfiltered data) and the operation has no documented success response; keep @blp.route outermost, or use MethodView

@blp.response(BookSchema) with the schema first

the status code comes first; fix the call (TypeError at import time)

Expecting @blp.response to validate output

it serializes only; test the output against the document

Raising ValidationError inside a view

it becomes a 500; use @blp.arguments or abort(422/409, ...)

Unknown fields silently dropped

Meta.unknown = EXCLUDE is tolerant, RAISE is strict; choose per schema and remember RAISE becomes additionalProperties: false

required=True on a query field has no visible effect

for non-body locations the requirement is per field in the schema; a schema with load_default is optional

Wrong or missing OPENAPI_VERSION

the three required settings must be present; pick 3.1.0 and validate the result

Blueprint registered with app.register_blueprint

it works but is missing from the document; use api.register_blueprint

Documentation endpoints exposed in production

set OPENAPI_URL_PREFIX conditionally, or authenticate the prefix

Unpinned CDN scripts

pin versions or self-host; they run with your origin’s privileges

securitySchemes without enforcement

the document does not authenticate anything; protect the views

Documented examples with real credentials

use obvious placeholders; generated documents and Swagger "Authorize" fields are often copied around

Generated document not reviewed

commit it and diff it in CI so API changes are visible and deliberate

Where the Books Are Outdated

The Manning book Microservice APIs (José Haro Peralta, 2022/2023) is a good guide to API design, and chapter 5 gives the anatomy of an OpenAPI document; for flask-smorest, its chapter 6 contains errors and outdated choices that this page corrects. The Packt Flask Framework Cookbook editions and Full-Stack Flask and React document APIs with Flask-RESTful or Postman collections instead of OpenAPI. In summary:

In the book Current behaviour

Meta.unknown = EXCLUDE described as rejecting unknown fields, and RAISE as ignoring them

it is the other way round: RAISE rejects (and is the default), EXCLUDE silently drops; see The Unknown-Field Policy

@blp.response(...) decorating above @blp.route(...)

@blp.route must be outermost; see The Decorators and Their Order

@blp.response(Schema) with the schema first

the status code is the first positional argument (since flask-smorest 0.29.0)

marshmallow ValidationError raised in a view treated as a 422

it is a 500; only @blp.arguments and abort produce formatted errors; see Responses, Errors and Status Codes

the served document replaced by monkeypatching APISpec.to_dict

a workaround only; serve your own file, or use a design-first framework; see Design-First with a Hand-Written Document

OpenAPI 3.0.3

3.1 is aligned with JSON Schema 2020-12 and is the better target for new documents; see Which OpenAPI Version

RFC 7231 and a custom error payload

RFC 7231 is superseded by RFC 9110; use RFC 9457 Problem Details; see Problem Details Instead of the Default Format

Flask-RESTful with reqparse (Cookbook, chapter 7)

reqparse is deprecated and the extension barely maintained; use flask-smorest, APIFlask or flask-openapi3

Dredd for contract testing

archived; use Schemathesis or a validator-based test; see Design-First with a Hand-Written Document

Postman collections as the API documentation (Full-Stack Flask and React)

a Postman collection documents requests, not the contract; generate or write an OpenAPI document and import it

Recent Changes

Version Change that matters here

flask-smorest 0.29.0 (2021-02-13)

the status code becomes the first positional argument of @blp.response

flask-smorest 0.40.0 (2022-10-16)

apispec 6 is required

flask-smorest 0.43.0 (2024-02-12)

requires Flask 3.0.2 or later and Werkzeug 3.0 or later

flask-smorest 0.46.1 (2025-04-26)

marshmallow 4 is supported

flask-smorest 0.47.0 (2026-03-22)

Python 3.9 dropped

flask-smorest 0.48.0 (unreleased at the check date)

drops marshmallow 3; the Python 3.14 support is announced in its changelog

Python 3.13

the generated names of the 422 response component follow http.HTTPStatus (UNPROCESSABLE_CONTENT on 3.13, UNPROCESSABLE_ENTITY on earlier versions), so a committed spec can differ between Python versions

OpenAPI 3.1 and later

JSON Schema 2020-12 dialect, webhooks; the specification site lists 3.2.1 as the latest published version

Summary

  • An OpenAPI document is the contract of the API; renderers, clients, mocks and tests are generated from it. Flask has no built-in support, so the document comes from an extension.

  • Code-first with flask-smorest: Api, Blueprint, MethodView, marshmallow schemas, and the decorators @blp.arguments, @blp.response, @blp.alt_response, @blp.paginate and @blp.doc, with @blp.route outermost.

  • The unknown-field policy decides strict or tolerant input; RAISE is the default and shows up as additionalProperties: false.

  • @blp.response serializes but does not validate; errors raised inside views are not formatted unless they are HTTPException instances; Problem Details is a subclass of Api away.

  • Set OPENAPI_VERSION to 3.1.0, serve the UIs from pinned or self-hosted scripts, and disable or protect them in production.

  • Commit the generated document (flask openapi write, or flask spec in APIFlask) and fail CI when it changes unexpectedly.

  • Design-first means a validated hand-written document plus contract tests; Connexion routes from the document, and Schemathesis and openapi-core automate the checks.