Skip to main content

Generate Pydantic v2 models and FastAPI routers from OpenAPI 3.0/3.1 specs

Project description

pyoas

Generate Pydantic v2 models and FastAPI routers from an OpenAPI spec. Organized as a uv workspace with independent, installable packages.

Packages

Package Purpose
pyoas Spec loading, ref resolution, tag extraction, Jinja2 rendering, Pydantic v2 model generation, CLI
pyoas[fastapi] FastAPI router generation + service stubs + test scaffolding (adds FastAPI dependency)
pyoas[claude] Claude Code skill generation (optional, no extra runtime dependencies)

pyoas[fastapi] and pyoas[claude] both extend the base pyoas package.

Quick start

# Install
uv add pyoas[fastapi]

# Create a minimal config
cat > pyoas.yaml << 'EOF'
spec: openapi.yaml
output:
  models: src/generated/models
  routers: src/generated/routers
EOF

# Generate Pydantic models and FastAPI routers
uv run pyoas generate

# Optionally scaffold service stubs and test files
uv run pyoas scaffold services

A more complete config with all optional features:

spec: openapi.yaml
output:
  models: src/generated/models
  routers: src/generated/routers
services:
  generate: true
  output: src/services
  import_path: myapp.services  # Python import path for service module
tests:
  generate: true
  output: tests/generated
  not_found_exception: "HTTPException(status_code=404, detail='Not found')"
skills:
  generate: true  # requires pyoas[claude]

Then:

uv run pyoas generate   # models + routers + service stubs + tests + skills

Configuration reference (pyoas.yaml)

spec

Path to the OpenAPI 3.0/3.1 spec file (YAML or JSON). Resolved relative to the config file. Required.

output

Key Default Description
models src/generated/models Output directory for generated model files
routers src/generated/routers Output directory for generated router files
models_import (derived) Python import path for models; derived from models path if omitted
routers_import (derived) Python import path for routers; derived from routers path if omitted
source_root src Filesystem prefix stripped when deriving Python import paths

default_tag

Default: "default". Operations with no tag are grouped under this name.

model_config

Key Default Description
extra "ignore" Pydantic extra setting for response/shared models
request_extra "forbid" Pydantic extra setting for request-only models
frozen false Makes generated models immutable
populate_by_name true Allow populating fields by Python name as well as alias
include_unreferenced false Also generate models for schemas not referenced by any operation

fields

Key Default Description
snake_case true Convert camelCase field names to snake_case with an alias
enums_as_literals true Render small enums as Literal[...] instead of Enum subclasses
unique_items_as_set true Render arrays with uniqueItems: true as set[T]; set to false to keep list[T]

format

Key Default Description
enabled true Run ruff format on generated files after writing

templates

Key Default Description
models null Path to a directory of custom Jinja2 templates overriding model templates
routers null Path to a directory of custom Jinja2 templates overriding router templates

services

Key Default Description
generate false Scaffold service stub files
output src/services Output directory for service files
overwrite false Overwrite existing service files on re-run
import_path "" Python import path used by routers to import the service (e.g. myapp.services)

tests

Key Default Description
generate false Scaffold pytest test stub files
output tests/generated Output directory for test files
overwrite false Overwrite existing test files on re-run (default: append new test classes only)
not_found_exception null Exception expression used in test_not_found stubs (e.g. HTTPException(status_code=404))

skills

Requires pyoas[claude] to be installed.

Key Default Description
generate false Generate Claude Code skill files
output .claude/commands Output directory for skill files
overwrite false Overwrite existing skill files on re-run

webhooks

OAS 3.1 webhooks are extracted but not generated by default.

Key Default Description
generate false Generate FastAPI routers for webhook operations (OAS 3.1 only)

extensions

Register custom Jinja2 filters and globals loaded at render time via importlib.

Key Default Description
filters null "module:attr" pointing to a callable returning dict[str, Callable]
globals null "module:attr" pointing to a callable returning dict[str, Any]

Example:

extensions:
  filters: myapp.pyoas_extensions:custom_filters
  globals: myapp.pyoas_extensions:custom_globals

plugins

List of plugin class specifiers loaded at generation time. Each entry is a "module:ClassName" string. Plugins can also be discovered automatically via pyproject.toml entry-points (group "pyoas.plugins").

plugins:
  - myapp_plugin:HeaderPlugin

See examples/plugin_example/ for a working example.

Generated output

Models (pyoas)

One file per tag: {models_output}/{tag}/models.py. Schemas referenced by multiple tags go to {models_output}/shared/models.py.

src/generated/models/
  __init__.py
  pets/
    models.py      # Pet, PetCreate, PetList, ...
  shared/
    models.py      # schemas used by more than one tag

Routers (pyoas[fastapi])

One file per tag: {routers_output}/{tag}/router.py. An __init__.py at the root re-exports all routers.

src/generated/routers/
  __init__.py      # from .pets import router as pets_router; ...
  pets/
    router.py      # APIRouter with typed endpoint stubs

Service stubs

One file per tag: {services_output}/{tag}.py. Scaffolded once; never overwritten by default.

src/services/
  pets.py          # PetsService class with async method stubs

Test scaffolding

One test file per tag plus a shared conftest.py with model factories.

tests/generated/
  conftest.py      # make_pet(), make_pet_list(), ...
  test_pets.py     # TestListPets, TestCreatePet, TestGetPet, ...

Each test class covers one endpoint and includes:

  • test_endpoint_exists — verifies the route returns something other than 404/405
  • Validation tests for required fields, numeric bounds, string constraints, enum violations
  • test_not_found — verifies 404 when the service raises the configured exception
  • test_success — happy-path stub (auto-implemented for GET/DELETE, stubbed for others)

CLI reference

Generation

pyoas models        # generate Pydantic models only
pyoas fastapi       # generate FastAPI routers only
pyoas generate      # generate models + routers (+ services/tests/skills if configured)

Generation commands accept:

  • --config PATH — path to config file (default: pyoas.yaml)
  • --tags TAG1,TAG2 — limit generation to specific tags
  • --clean — purge output directory before generating
  • --quiet — suppress progress output (errors still shown)
  • --verbose — show per-tag timing alongside progress

Scaffolding

pyoas scaffold services       # scaffold service stubs (skips existing files)
pyoas scaffold tests          # scaffold pytest test files (skips existing files)
pyoas scaffold dependencies   # scaffold auth dependency stubs
pyoas scaffold skills         # scaffold Claude Code skill files
pyoas scaffold webhooks       # print webhook router mount instructions

Diagnostics

pyoas doctor         # pre-flight checks: missing operationIds, broken refs, tag collisions, …
pyoas validate       # parse and validate the spec file; exits non-zero on error
pyoas diff           # dry-run generation and report added/removed/changed files
pyoas drift          # detect service methods that are missing or out of sync with the spec

doctor and validate accept --json to emit structured JSON instead of coloured text.

Maintenance

pyoas fix            # auto-fix common spec issues (assign operationIds, deduplicate, normalise tags)
pyoas fix --dry-run  # show what would be changed without writing the file
pyoas fix --tag-casing lower  # normalise tags to lowercase (default: title-case)

pyoas migrate OLD_SPEC NEW_SPEC          # diff two specs and classify breaking changes
pyoas migrate OLD NEW --json             # structured JSON output
pyoas migrate OLD NEW --breaking-only   # suppress non-breaking changes (useful in CI)

migrate exits non-zero when breaking changes are found.

Initialisation

pyoas init openapi.yaml            # generate a starter pyoas.yaml
pyoas init openapi.yaml --force    # overwrite existing config

Plugin architecture

Plugins let you post-process generated files without forking pyoas. A plugin is a plain Python class with four lifecycle hooks:

from typing import Any

class MyPlugin:
    name = "my_plugin"
    version = "1.0.0"

    def on_spec_loaded(self, spec: dict, resolved: dict) -> tuple[dict, dict]:
        return spec, resolved  # return unchanged, or modify and return

    def on_model_file_written(self, tag: str, path: str, content: str) -> str:
        return content  # return modified content (must be non-empty)

    def on_router_file_written(self, tag: str, path: str, content: str) -> str:
        return content

    def on_generate_complete(self, stats: dict[str, Any]) -> None:
        print(f"Done: {stats['files_written']} file(s) written")

Activate via pyoas.yaml:

plugins:
  - myapp.plugin:MyPlugin

Or via pyproject.toml entry-points for distributable plugins:

[project.entry-points."pyoas.plugins"]
my_plugin = "myapp.plugin:MyPlugin"

pyoas doctor validates plugin imports before generation starts. See examples/plugin_example/ for a complete working example.

Claude Code integration (pyoas[claude])

Install pyoas[claude] and set skills.generate: true in your config. Running pyoas generate will write Claude Code skill files to .claude/commands/:

Skill Invocation Purpose
implement-tests.md /implement-tests tests/generated/test_pets.py Implement all pytest.skip("implement me") stubs in a test file
add-test-case.md /add-test-case tests/generated/test_pets.py "scenario" Add a new test method for the described scenario
review-generated.md /review-generated Cross-reference generated code against the OpenAPI spec and flag issues

Development

# Install in editable mode with all extras
uv sync --extra fastapi --extra claude

# Run all tests
uv run pytest

# Run tests for a single area
uv run pytest tests/fastapi/

# Update snapshots
uv run pytest tests/fastapi/ --snapshot-update

# Lint and type-check
uv run ruff check src/
uv run mypy src/

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyoas-0.5.2.tar.gz (94.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyoas-0.5.2-py3-none-any.whl (123.9 kB view details)

Uploaded Python 3

File details

Details for the file pyoas-0.5.2.tar.gz.

File metadata

  • Download URL: pyoas-0.5.2.tar.gz
  • Upload date:
  • Size: 94.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.15 {"installer":{"name":"uv","version":"0.11.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for pyoas-0.5.2.tar.gz
Algorithm Hash digest
SHA256 ffcf8cbc76477fdf62de5c4b37a46ebae87781620adbb950001200090cd53b99
MD5 35094830be3a1c728fc10a755f6bb424
BLAKE2b-256 ba575f18db95ae6804a70ee2dcba7050c80707e709b118dd711674d2cc2701d7

See more details on using hashes here.

File details

Details for the file pyoas-0.5.2-py3-none-any.whl.

File metadata

  • Download URL: pyoas-0.5.2-py3-none-any.whl
  • Upload date:
  • Size: 123.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.15 {"installer":{"name":"uv","version":"0.11.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for pyoas-0.5.2-py3-none-any.whl
Algorithm Hash digest
SHA256 a6c0a620c009383c3448551c43c1a67f399e24c3b0b7d87ed388fc68dcd49346
MD5 fce9b6ce445ccde23964ff04e22f2440
BLAKE2b-256 c395f66e366e98226a4bbcadea4e0718d9d102f3949f78d908670a285f00c947

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page