openapi-pydantic
openapi-pydantic is the OpenAPI 3.1 (and 3.0) specification expressed as Pydantic models. Every object in the spec has a matching class: OpenAPI, Info, PathItem, Operation, Response, Parameter, Schema, SecurityScheme, Reference. You build a spec by constructing those objects, or by validating a plain dict into them, and Pydantic tells you when a field is wrong instead of a downstream tool failing hours later. It also ships a utility that walks your document, finds placeholders pointing at your own Pydantic request and response models, generates their JSON Schema, and moves them into components/schemas with proper $ref links. It works with both Pydantic 1.8+ and Pydantic 2 through an internal compatibility module. It is a modeling library, not a server framework: it does not route requests, validate traffic, or serve a docs page.
The cleanest way to build an OpenAPI 3.1 document in Python when you are not on FastAPI, and the typed models catch spec mistakes that dicts hide. Weigh the quiet release history before you put it on a critical build path.
Use it if
- You hand-write or assemble an OpenAPI document in Python and want the spec typed, so a misspelled field or a missing Response description is a validation error at build time
- Your request and response bodies are already Pydantic models and you want their JSON Schema injected into components/schemas automatically instead of copy-pasting generated schema blobs
- You are writing a tool that reads arbitrary OpenAPI documents (a linter, a client generator, a diff checker) and you want to work against typed attributes rather than nested dict lookups
- You need OpenAPI 3.1 specifically, which many older Python spec libraries never added support for
- You are on a framework without built-in spec generation (Flask, aiohttp, a plain ASGI app) and are producing openapi.json yourself
- You use FastAPI: it already builds a validated OpenAPI document from your routes and Pydantic models, and adding this library gives you a second definition of the same API to keep in sync
- Release cadence matters to you: 0.5.1 was published in January 2025 and the repository was last pushed in November 2025, so at time of writing there has been no release for well over a year and it is effectively one maintainer's project
- You just want to check that a spec file is valid: openapi-spec-validator does that in three lines without you building a model tree
- You are going the other direction, from an existing spec to Python code: this library does not generate clients or server stubs, and datamodel-code-generator is the tool for turning a spec into Pydantic models
- You want documentation: there is one README, no hosted docs site, and finding the attribute name for a field like the parameter location means reading the source (it is param_in, aliased to in)
- Your consumers render specs with older Swagger UI builds that choke on 3.1: you can import the 3.0 models instead, but then you are on a second, less-exercised model tree
Setup reality
pip install openapi-pydantic pulls only Pydantic (1.8 or newer, including 2.x), so there is nothing to compile and no extras to pick. The friction is entirely in usage. Field names that collide with Python keywords are renamed and aliased, so Parameter takes param_in and SecurityScheme takes security_scheme_in while the serialized key is in, and Reference takes ref for $ref. That means you must dump with by_alias=True or the output is not valid OpenAPI, and you also want exclude_none=True or every optional field appears as null and confuses downstream tooling. Pydantic 1 users call json() and parse_obj(); Pydantic 2 users call model_dump_json() and model_validate(), and every README snippet has to be mentally translated to whichever you are on. The default OpenAPI version is 3.1.1; if you need 3.0 you import from openapi_pydantic.v3.v3_0 everywhere, including the util module, and mixing the two import paths produces confusing validation errors.
Patterns
Construct a spec from typed objectsbuild-minimal-spec
from openapi_pydantic import OpenAPI, Info, PathItem, Operation, Response
spec = OpenAPI(
info=Info(title="Orders API", version="1.0.0"),
paths={
"/ping": PathItem(
get=Operation(
responses={"200": Response(description="pong")}
)
)
},
)Response.description is required by the spec, so omitting it raises a Pydantic validation error here instead of producing a document that fails in a validator later.
Serialize to a valid openapi.jsondump-valid-json
# Pydantic 2
print(spec.model_dump_json(by_alias=True, exclude_none=True, indent=2))
# Pydantic 1
print(spec.json(by_alias=True, exclude_none=True, indent=2))Both flags are mandatory. Without by_alias you emit param_in and ref instead of in and $ref; without exclude_none every unset optional field shows up as null.
Load a spec file and get typed accessparse-existing-spec
import json
from openapi_pydantic import parse_obj
with open("openapi.json") as f:
spec = parse_obj(json.load(f))
for path, item in spec.paths.items():
if item.get is not None:
print(path, item.get.operationId, list(item.get.responses))parse_obj reads the top-level openapi field and picks the 3.0 or 3.1 model tree for you. Calling OpenAPI.model_validate directly always uses 3.1 and will reject some 3.0-only shapes.
Reference your own models as request and response schemaspydantic-models-as-schemas
from pydantic import BaseModel
from openapi_pydantic import OpenAPI
from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class
class OrderIn(BaseModel):
sku: str
qty: int
base = OpenAPI.model_validate({
"info": {"title": "Orders API", "version": "1.0.0"},
"paths": {"/orders": {"post": {
"requestBody": {"content": {"application/json": {
"schema": PydanticSchema(schema_class=OrderIn)
}}},
"responses": {"201": {"description": "created"}},
}}},
})
spec = construct_open_api_with_schema_class(base)The function returns a new document; the input is left alone. It replaces each PydanticSchema placeholder with a $ref and puts the generated JSON Schema under components/schemas.
Emit 3.0 for tools that reject 3.1target-openapi-30
from openapi_pydantic.v3.v3_0 import OpenAPI, Info, PathItem, Operation, Response
from openapi_pydantic.v3.v3_0.util import (
PydanticSchema,
construct_open_api_with_schema_class,
)Import every symbol from the v3_0 path, including the util helpers. Mixing a v3_1 Operation into a v3_0 OpenAPI gives a validation error that does not mention versions at all.
Mix dicts and model instances while buildingmixed-dict-and-objects
from openapi_pydantic import OpenAPI, PathItem, Response
spec = OpenAPI.model_validate({
"info": {"title": "Orders API", "version": "1.0.0"},
"paths": {
"/ping": PathItem(
get={"responses": {"200": Response(description="pong")}}
)
},
})Handy for porting an existing dict-based generator one endpoint at a time rather than rewriting the whole thing to typed objects in one commit.
Add path and query parametersdeclare-parameters
from openapi_pydantic import Operation, Parameter, Response, Schema
op = Operation(
parameters=[
Parameter(name="orderId", param_in="path", required=True,
param_schema=Schema(type="string")),
Parameter(name="expand", param_in="query",
param_schema=Schema(type="boolean")),
],
responses={"200": Response(description="an order")},
)in and schema are both awkward attribute names in Python, so the fields are param_in and param_schema with aliases. Passing in= or schema= as keywords is a TypeError.
Declare bearer auth in componentssecurity-scheme
from openapi_pydantic import OpenAPI, Components, SecurityScheme, Info
spec = OpenAPI(
info=Info(title="Orders API", version="1.0.0"),
paths={},
components=Components(
securitySchemes={
"bearerAuth": SecurityScheme(
type="http", scheme="bearer", bearerFormat="JWT"
)
}
),
security=[{"bearerAuth": []}],
)For an apiKey scheme the location field is security_scheme_in, not in, for the same keyword-collision reason as Parameter.
Point at a component you defined yourselfmanual-component-ref
from openapi_pydantic import MediaType, Reference, Response
resp = Response(
description="an order",
content={
"application/json": MediaType(
media_type_schema=Reference(ref="#/components/schemas/Order")
)
},
)Reference takes ref, aliased to $ref. Nothing checks that the target exists, so a typo here surfaces only when a consumer tries to resolve the document.
Fail the build on an invalid specvalidate-spec-in-ci
import json, sys
from pydantic import ValidationError
from openapi_pydantic import parse_obj
try:
parse_obj(json.load(open("openapi.json")))
except ValidationError as e:
print(e)
sys.exit(1)This checks the document against the spec's object model, not against your API's behaviour. It will not notice that an endpoint you deleted is still documented.
Diff the generated spec against the committed onedetect-spec-drift
import json
from myapp.openapi import build_spec
fresh = json.loads(build_spec().model_dump_json(by_alias=True, exclude_none=True))
committed = json.load(open("openapi.json"))
assert fresh == committed, "openapi.json is stale, regenerate it"Compare parsed dicts, not raw text. Key ordering and indentation change between Pydantic patch releases and would give you a false failure every upgrade.
Drop to a raw dict for a field the models do not coverraw-schema-escape-hatch
from openapi_pydantic import Info, OpenAPI
spec = OpenAPI(info=Info(title="Orders API", version="1.0.0"), paths={})
doc = spec.model_dump(by_alias=True, exclude_none=True)
doc["x-internal-owner"] = "payments-team"A few spec constructs (security requirement maps, some extension points) are plain dicts rather than models, and vendor x- extensions are easiest to attach after dumping.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| fastapi | PyPI | You are building the API itself and want the spec derived from your route handlers rather than written by hand |
| apispec | PyPI | You want a plugin-based spec builder that reads docstrings and marshmallow schemas instead of modeling the document as objects |
| datamodel-code-generator | PyPI | You already have an OpenAPI file and want Pydantic models generated from it, which is the reverse of what this library does |
| openapi-spec-validator | PyPI | All you need is a yes or no on whether an existing spec file is valid |