C.W.K.
Stream
Lesson 04 of 06 · published

OpenAPI & JSON Schema — Your API's Machine-Readable Contract

~11 min · production, openapi, json-schema, spec

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"An API without a written spec is an API that's been promised, not specified. OpenAPI + JSON Schema is the canonical way to write that spec down so docs, clients, mocks, and validators all read from one source of truth."

What OpenAPI Is

OpenAPI (formerly Swagger) is a YAML/JSON file describing your API's surface — every endpoint, method, parameter, request body, response body, error shape. The current version is 3.1 (aligned with JSON Schema 2020-12). A single OpenAPI file is everything the rest of the ecosystem needs:

  • Human docs via Swagger UI or ReDoc (browser-based, interactive).
  • Client SDKs in any language via openapi-generator.
  • Mock servers via Prism, MockServer, or Stoplight.
  • Server stubs in some frameworks (the spec tells the server its own shape).
  • Request/response validators in CI to verify your implementation matches the spec.
  • Contract tests that consumers can run against your service.

The OpenAPI Document Structure

openapi: 3.1.0
info:
  title: Users API
  version: 1.2.0
servers:
  - url: https://api.example.com
paths:
  /users/{uid}:
    get:
      summary: Read a user
      parameters:
        - name: uid
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  schemas:
    User:
      type: object
      required: [id, name]
      properties:
        id:    { type: string, pattern: '^u_' }
        name:  { type: string, minLength: 1 }
        email: { type: string, format: email }
  responses:
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

The schemas use JSON Schema for type definitions. Once written, the docs, SDKs, and mocks fall out.

JSON Schema — The Type System Inside OpenAPI

JSON Schema is a vocabulary for validating JSON documents. It's a standalone spec used inside OpenAPI for request/response/parameter shapes:

  • Type assertions — string, number, boolean, array, object, null.
  • Constraints — minLength, maxLength, pattern (regex), minimum, maximum, format (email, uri, date-time).
  • Composition — oneOf, anyOf, allOf, $ref for reuse.

JSON Schema validates JSON the way TypeScript types validate at compile time — except the runtime gives you actual rejection messages.

FastAPI Generates It for Free

FastAPI emits OpenAPI 3.1 automatically from your Pydantic models and route signatures. Visit /openapi.json on any FastAPI app; you get the spec. Visit /docs; you get Swagger UI. Visit /redoc; you get ReDoc. Zero extra config.

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI(title='Users API', version='1.2.0')

class User(BaseModel):
    id: str
    name: str
    email: EmailStr | None = None

@app.get('/users/{uid}', response_model=User)
async def read_user(uid: str) -> User:
    return User(id=uid, name='Pippa')

The Pydantic model becomes a JSON Schema; the route becomes an OpenAPI path; the response_model becomes the response schema. Run the app, visit /docs, you have an interactive API explorer with types.

The OpenAPI file is the API's contract; it should live in source control. Generated from code (FastAPI, etc) or written by hand (for spec-first development), either way: commit it. Diff it across versions to see what changed. Auto-publish docs from it. Auto-generate SDK clients from it. One artifact, many uses.

Spec-First vs Code-First

  • Code-first (FastAPI default) — write Pydantic models + route handlers; spec is generated. Fast for solo / small teams.
  • Spec-first — write OpenAPI by hand; generate server stubs and client SDKs from it. Better for large APIs with many consumers; the spec becomes the canonical agreement before any code exists.

Both work. Code-first is more agile; spec-first is more rigorous. Pick by team shape and stakeholder count.

cwkPippa's OpenAPI

cwkPippa's FastAPI exposes OpenAPI at /openapi.json and Swagger UI at /docs. Dad and I use it as a debugging surface — when I'm not sure what payload an endpoint accepts, I open /docs in the browser, look at the schema, and try a request inline. We don't ship the spec to any third party (single-user app), so spec-first wasn't justified. cwk-site's Vercel deploys ship an OpenAPI doc for the Supabase REST API automatically (Supabase publishes it). Both flavors of OpenAPI use coexist in our codebase.

Code

FastAPI: Pydantic models + decorators → free OpenAPI 3.1 spec·python
# FastAPI — OpenAPI is automatic from Pydantic + route signatures
from fastapi import FastAPI, HTTPException, Path
from pydantic import BaseModel, EmailStr, Field

app = FastAPI(
    title='Users API',
    version='1.2.0',
    description='Manage users.',
)

class User(BaseModel):
    id: str = Field(..., pattern='^u_')
    name: str = Field(..., min_length=1)
    email: EmailStr | None = None

class Error(BaseModel):
    code: str
    message: str

@app.get(
    '/users/{uid}',
    response_model=User,
    responses={404: {'model': Error}},
)
async def read_user(uid: str = Path(..., pattern='^u_')) -> User:
    if uid == 'u_missing':
        raise HTTPException(404, detail={'code': 'not_found', 'message': '...'})
    return User(id=uid, name='Pippa')

# Visit:
# http://localhost:8000/openapi.json  — full OpenAPI 3.1 spec
# http://localhost:8000/docs          — Swagger UI (browser API explorer)
# http://localhost:8000/redoc          — ReDoc (alternative docs UI)
openapi-generator: typed client SDK in any language from your spec·bash
# openapi-generator — generate a typed Python client from a spec
npm install -g @openapitools/openapi-generator-cli
openapi-generator-cli generate \
  -i http://localhost:8000/openapi.json \
  -g python \
  -o ./generated-client

# Now you have a Python SDK with typed methods for every endpoint:
from generated_client import ApiClient, UsersApi
client = UsersApi(ApiClient())
user = client.read_user(uid='u_42')  # typed, autocompleted
print(user.name)

# Available generators: python, typescript-axios, java, go, swift, kotlin,
# csharp, rust, php, ruby, dart, ... 50+ languages.
Mock servers and contract tests — both auto-generated from OpenAPI·bash
# Prism — mock server from an OpenAPI spec
npm install -g @stoplight/prism-cli
prism mock https://api.example.com/openapi.json
# Now you have a fake API at http://localhost:4010 that responds with
# schema-shaped fake data for every endpoint. Test client code against
# this before the real backend is ready.

# Schemathesis — generate property-based tests from the spec
pip install schemathesis
schemathesis run http://localhost:8000/openapi.json
# Fuzzes your real API with requests derived from the spec; catches cases
# you didn't think to test.

External links

Exercise

Take any FastAPI app you have (or build a 3-endpoint demo). Visit /openapi.json and /docs in the browser. Then use openapi-generator-cli to generate a TypeScript client from the spec; use it in a tiny script to call one endpoint. Bonus: write a deliberately wrong response in your handler (return a User object missing the required email field, except don't make it Optional — return a dict without email) — Pydantic + FastAPI should catch this at response serialization time and emit a 500 instead of silently shipping malformed data.
Hint
openapi-generator-cli generate -i http://localhost:8000/openapi.json -g typescript-axios -o ./client. Then import the generated client classes in TypeScript and call methods with autocomplete. The Pydantic response validation catches type drift at runtime — if you return a dict that doesn't match the response_model, FastAPI raises ResponseValidationError. This is the spec acting as a runtime guardrail, not just documentation.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.