mrkeyoor.com_
Thu 06 Aug 10:56 UTC
PyPIUtilsupdated 06 Aug 2026

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.

Verdict

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.

API stability4/5Still on a 0.x version, but the class surface is dictated by the OpenAPI specification itself so it barely moves; the real churn risk is the Pydantic 1 versus 2 split, where the method you call to dump or parse differs by which Pydantic you installed.
Docs3/5The README is well organised, honest about the by_alias and exclude_none requirement, and shows the Pydantic-models-as-schema flow end to end. There is no hosted reference, so field-level questions (aliases, which types are plain dicts rather than models) send you into the package source.
Maintenance2/5Version 0.5.1 dates to 8 January 2025 and the last repository push was 24 November 2025, with 10 open issues and a single primary maintainer. It is not abandoned, and it has already outlived the openapi-schema-pydantic project it forked from, but nothing has shipped in a long time.
Ecosystem4/5Around 16M installs a week against 122 GitHub stars, which is the signature of a package most people receive as somebody else's dependency rather than pick themselves. That means it is widely present and well exercised, but there is little community tooling, few blog posts, and a small issue tracker to search when you get stuck.

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
Skip it if

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

PackageRegistryPick it when
fastapiPyPIYou are building the API itself and want the spec derived from your route handlers rather than written by hand
apispecPyPIYou want a plugin-based spec builder that reads docstrings and marshmallow schemas instead of modeling the document as objects
datamodel-code-generatorPyPIYou already have an OpenAPI file and want Pydantic models generated from it, which is the reverse of what this library does
openapi-spec-validatorPyPIAll you need is a yes or no on whether an existing spec file is valid