OpenAPI·Python·Free

OpenAPI to
Python.

Turn an OpenAPI 3.0 or 3.1 spec into a typed Python client. httpx.AsyncClient by default, Pydantic v2 models with model_validate and model_dump, modern union syntax like str | None. One file per model. Opt in to sync wrappers when you need them. No signup, no install.

OpenAPI 3.0 + 3.1·Python 3.10+·httpx·Pydantic v2·MIT
payments.openapi.yamlInput
openapi: 3.0.3
info:
  title: Payments API
  version: 1.2.0
paths:
  /payments/{id}:
    get:
      operationId: getPayment
      parameters:
        - name: id
          in: path
          schema: { type: string }
        - name: expand
          in: query
          schema: { type: string }
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Payment'
components:
  schemas:
    Payment:
      type: object
      required: [id, amount, method]
      properties:
        id: { type: string }
        amount: { type: number }
        currency:
          type: string
          nullable: true
        method:
          oneOf:
            - $ref: '#/components/schemas/CardPayment'
            - $ref: '#/components/schemas/BankPayment'
          discriminator:
            propertyName: kind
compile
payments/services/default_service.py tsc cleanOutput
# payments/services/default_service.py
import httpx
from models.payment import Payment

class DefaultService:
    """HTTP client for Default operations."""

    def __init__(self, base_url: str, client: httpx.AsyncClient | None = None):
        self.base_url = base_url
        self.client: httpx.AsyncClient = client or httpx.AsyncClient()

    async def get_payment(self, id: str | None = None, expand: str | None = None) -> Payment:
        url = f"{self.base_url}/payments/{id}"
        params = {k: str(v) for k, v in {
            "expand": expand,
        }.items() if v is not None}

        response = await self.client.get(url, params=params)
        response.raise_for_status()
        return Payment.model_validate(response.json())
What gets generated

Idiomatic Python. Pydantic v2 all the way down.

Below: models emitted from a small payments spec with a oneOf discriminator. Each model is its own file; the union is a Pydantic Annotated TypeAlias with a Field(discriminator=...) tag — Pydantic dispatches the variant for you and your code can branch on the discriminator field.

# payments/models/payment.py
from __future__ import annotations
from pydantic import BaseModel, ConfigDict
from union_types import MethodUnion
from models.payment_status import PaymentStatus

class Payment(BaseModel):
    model_config = ConfigDict(populate_by_name=True)
    status: PaymentStatus
    id: str
    amount: float
    currency: str | None = None
    method: MethodUnion


# payments/union_types.py
from typing import Annotated, TypeAlias, Union
from pydantic import Field
from models.card_payment import CardPayment
from models.bank_payment import BankPayment

MethodUnion: TypeAlias = Annotated[Union[CardPayment, BankPayment], Field(discriminator='kind')]


# Narrowing on the discriminator (yours to write — the types enable it):
def label(m: MethodUnion) -> str:
    if m.kind == "card":
        return f"card {m.last4}"
    return f"bank {m.iban}"
Three things we get right

Grounded in how the spec reads.

Each claim here is something you can verify by running the converter on your own spec. Click a card to see the spec fragment that triggered it and the code that came out.

01ProofPydantic v2 models, native v2 API.Spec · InCode · Out
spec.yaml fragment
method:
  oneOf:
    - $ref: '#/components/schemas/CardPayment'
    - $ref: '#/components/schemas/BankPayment'
  discriminator:
    propertyName: kind
emitted .py files
# union_types.py
from typing import Annotated, TypeAlias, Union
from pydantic import Field
from models.card_payment import CardPayment
from models.bank_payment import BankPayment

MethodUnion: TypeAlias = Annotated[Union[CardPayment, BankPayment], Field(discriminator='kind')]


# models/card_payment.py
from __future__ import annotations
from pydantic import BaseModel, ConfigDict
from models.card_payment_kind import CardPaymentKind

class CardPayment(BaseModel):
    model_config = ConfigDict(populate_by_name=True)
    kind: CardPaymentKind
    last4: str


# models/card_payment_kind.py
from enum import Enum

class CardPaymentKind(str, Enum):
    CARD = "card"
Python · Async (default)

Async methods, reusable client.

The default output. Every operation is async, the service takes an httpx.AsyncClient you can inject or let it build its own, and responses are validated through Pydantic v2 before you get them.

payments/services/default_service.py generated
# payments/services/default_service.py
import httpx
from models.payment import Payment
from models.create_payment_request import CreatePaymentRequest

class DefaultService:
    """HTTP client for Default operations."""

    def __init__(self, base_url: str, client: httpx.AsyncClient | None = None):
        self.base_url = base_url
        self.client: httpx.AsyncClient = client or httpx.AsyncClient()

    async def get_payment(self, id: str | None = None, expand: str | None = None) -> Payment:
        url = f"{self.base_url}/payments/{id}"
        params = {k: str(v) for k, v in {
            "expand": expand,
        }.items() if v is not None}

        response = await self.client.get(url, params=params)
        response.raise_for_status()
        return Payment.model_validate(response.json())

    async def create_payment(self, body: CreatePaymentRequest) -> Payment:
        url = f"{self.base_url}/payments"
        json_data = body.model_dump(by_alias=True, exclude_none=True)

        response = await self.client.post(url, json=json_data)
        response.raise_for_status()
        return Payment.model_validate(response.json())
Also available via MCP

Or ask Claude to do it.

The same generator ships as a Model Context Protocol server. It calls the same web API this converter uses, so the output is identical. Point Claude Desktop, Cursor, or any MCP-aware agent at a spec and it produces the Python client as a diff — free, no token, no leaving the chat.

// claude-desktop config · .mcp.json
{
  "mcpServers": {
    "metaengine": {
      "command": "npx",
      "args": ["-y", "@metaengine/mcp-server"]
    }
  }
}

// Then in Claude:
// "Load specs/payments.yaml and generate a Python
//  client with async httpx methods and Pydantic v2 models."
See the MCP page
Prefer it in your build?

Same generator, as a package.

The converter above is the fastest way to try the output. For CI and repeatable builds, the identical generator ships as a package — pin a version, wire it into your pipeline, and review diffs like any other code.

Questions

Things people ask.

The same questions we keep getting. If yours is not here, run the converter — the answer is usually in the output.

A tree of .py files: one Pydantic v2 model per schema, one str, Enum per enum, a union_types.py for any oneOf discriminated unions, and an async service class per tag that uses httpx.AsyncClient. No runtime framework you did not ask for.
More converters

Same engine, other stacks.

Every converter runs the same spec-to-IR pipeline. Pick another source format or target stack.

OpenAPITypeScript
Angular httpResource, React TanStack Query, or framework-agnostic fetch.
Open converter
OpenAPIAngular
Typed services with httpResource, Signals, inject() DI, and interceptors.
Open converter
OpenAPIReact
TanStack Query hooks with stable query keys, or plain async functions.
Open converter
OpenAPIFetch
Framework-agnostic fetch client with ApiResult, middleware, and retries.
Open converter
OpenAPIJava Spring
RestClient services, Java records, sealed interfaces, Spring Boot 3.
Open converter
OpenAPIGo
Idiomatic net/http client, context-aware, pointer-nullable fields.
Open converter
OpenAPIKotlin
Ktor client, kotlinx.serialization, sealed interfaces for oneOf.
Open converter
OpenAPIC#
Records, nullable reference types, JsonPolymorphic, HttpClient.
Open converter
OpenAPIRust
reqwest client with serde-tagged enums for oneOf and Option<T>.
Open converter
GraphQLAngular
Observable services with typed queries, mutations, and graphql-ws subscriptions.
Open converter
GraphQLReact
TanStack Query hooks with discriminated-union types and query keys.
Open converter
GraphQLKotlin
Ktor, kotlinx.serialization, Flow-based subscriptions.
Open converter
GraphQLC#
Records, nullable types, [JsonPolymorphic] unions, IAsyncEnumerable subscriptions.
Open converter
ProtobufKotlin
Ktor, coroutines, and sealed classes straight from .proto.
Open converter
ProtobufC#
Records, nullable types, Connect RPC over HttpClient.
Open converter
ProtobufGo
Plain structs with json tags, pointer presence, stdlib-only Connect client.
Open converter
SQLTypeScript
Postgres DDL to typed interfaces with foreign-key navigation.
Open converter
SQLKotlin
CREATE TABLE scripts to Kotlin @Serializable data classes.
Open converter

Try it on your own spec.

The converter runs in the browser. Your spec never leaves the page.

Open the converter