{ } Schemato

Guide

How to turn JSON Schema into Pydantic v2 models

A practical walkthrough for FastAPI teams: take a JSON Schema document (with $ref, required, oneOf), produce Pydantic v2 models, plug them straight into your routes.

Need it now? Skip ahead to the JSON Schema → Pydantic converter.


Step 1 — Have a JSON Schema document

You can use a standalone JSON Schema, or pull one out of your OpenAPI spec — anything under components.schemas.Xis already a JSON Schema. Here's a typical customer document with a $ref:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Customer",
  "type": "object",
  "required": ["id", "name", "address"],
  "properties": {
    "id": { "type": "string" },
    "name": { "type": "string" },
    "email": { "type": "string", "format": "email" },
    "address": { "$ref": "#/$defs/Address" }
  },
  "$defs": {
    "Address": {
      "type": "object",
      "required": ["line1", "country"],
      "properties": {
        "line1": { "type": "string" },
        "city": { "type": "string" },
        "country": { "type": "string" }
      }
    }
  }
}

Step 2 — Generate Pydantic models

Paste the schema into the JSON Schema → Pydantic converter. The result resolves $defs into named classes, marks non-required fields as Optional[X] = None:

from __future__ import annotations

from typing import Any, Optional
from pydantic import BaseModel


class Address(BaseModel):
    line1: str
    city: Optional[str] = None
    country: str


class Customer(BaseModel):
    id: str
    name: str
    email: Optional[str] = None
    address: Address

Step 3 — Plug into FastAPI

Drop the generated class into a route parameter. FastAPI uses Pydantic for parsing, validation, and OpenAPI documentation — all three for free:

from fastapi import FastAPI
from .models import Customer

app = FastAPI()

@app.post("/customers")
async def create_customer(customer: Customer) -> Customer:
    # 'customer' is fully validated. Bad payloads return HTTP 422.
    return customer

A bad request body (missing fields, wrong types) returns HTTP 422 with a structured list of errors that points at exactly which field failed.

Step 4 — Refine with discriminators (oneOf)

JSON Schema's oneOf with a discriminator becomes a Pydantic discriminated union. The generator produces a generic Union — tighten it by hand:

from typing import Literal, Union
from pydantic import BaseModel, Field


class CardPayment(BaseModel):
    kind: Literal["card"]
    last4: str


class WalletPayment(BaseModel):
    kind: Literal["wallet"]
    provider: str


Payment = Union[CardPayment, WalletPayment]


class Order(BaseModel):
    id: str
    payment: Payment = Field(discriminator="kind")

Step 5 — Decide on strictness


Common pitfalls

FAQ

Does the converter understand $ref?

Yes — same-document references in $defs / definitions / components.schemas become named classes.

How are oneOf and anyOf translated?

They become Union types. Add a Field(discriminator=...) manually for tagged variants.

Do I need Pydantic v2?

The generated code uses v2 idioms (list[X] type hints, model_config). Targets Python 3.10+.

What about email or date-time formats?

Strings stay as str. Swap to EmailStr or datetime when you want stricter parsing.

Related