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 exceptiontest_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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyoas-0.7.1.tar.gz.
File metadata
- Download URL: pyoas-0.7.1.tar.gz
- Upload date:
- Size: 97.5 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2bf75f0f9c0cf5a4c0fa274b4fc92784b001907a4e7c4dfcedb27a85a6d35709
|
|
| MD5 |
61b93ca71e7f20e8add1d6266535f3e0
|
|
| BLAKE2b-256 |
8e2ff2fdfca4627f1c9092153498c80845f1d9b3f36a97f6c74d5523aaf8714d
|
File details
Details for the file pyoas-0.7.1-py3-none-any.whl.
File metadata
- Download URL: pyoas-0.7.1-py3-none-any.whl
- Upload date:
- Size: 126.7 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c822860b2582c82ac7d2b8d801e4034960111ac5f3836cc0e35d464d52548f43
|
|
| MD5 |
35f3dd34eb47f3df11223836f56fd2e0
|
|
| BLAKE2b-256 |
2b2d49611e6354030c7fd16cc6a9f83d9ba840935350a771d61c7872e65d0fce
|