diff --git a/openapi/frameworks/pydantic.mdx b/openapi/frameworks/pydantic.mdx index 1d342e1d..63cc1418 100644 --- a/openapi/frameworks/pydantic.mdx +++ b/openapi/frameworks/pydantic.mdx @@ -1,25 +1,58 @@ --- -title: How To Generate an OpenAPI Document With Pydantic V2 -description: "How to generate OpenAPI documents and great SDKs for your Pydantic V2 Models" +title: How To Generate an OpenAPI Document With Pydantic +description: "Generate OpenAPI from Pydantic models for SDK generation all from the same source of truth." --- -# How to generate an OpenAPI document with Pydantic V2 +# How to generate an OpenAPI document with Pydantic -[Pydantic](https://docs.pydantic.dev/latest/) is considered by many API developers to be the best data validation library for Python, and with good reason. By defining an application's models in Pydantic, developers benefit from a vastly improved development experience, runtime data validation and serialization, and automatic OpenAPI document generation. +[Pydantic](https://docs.pydantic.dev/latest/) is considered by many API +developers to be the best data validation library for Python, and with good +reason. By defining an application's models in Pydantic, developers benefit from +a vastly improved development experience, runtime data validation and +serialization, and automatic OpenAPI document generation. -However, many developers don't realize they can generate OpenAPI documents from their Pydantic models, which they can then use to create SDKs, documentation, and server stubs. +However, many developers don't realize they can generate OpenAPI documents from +their Pydantic models, which they can then use to create SDKs, documentation, +and server stubs, all of which are powered by the same source of truth. -In this guide, you'll learn how to create new Pydantic models, generate an OpenAPI document from them, and use the generated schema to create an SDK for your API. We'll start with the simplest possible Pydantic model and gradually add more features to show how Pydantic models translate to OpenAPI documents. +This guide uses a simple train travel sample app to show how Pydantic models +become OpenAPI schemas that can be used for SDK generation. The example project +lives in +[examples/framework-pydantic](https://github.com/speakeasy-api/examples/tree/main/examples/framework-pydantic), +and it exposes endpoints for stations, trips, and bookings. + +## Pydantic, JSON Schema, and OpenAPI + +Pydantic is a Python library for validating and shaping data. It can generate a +JSON Schema from Python models, which is useful for a lot more than just API +descriptions: code generation, form validation, documentation, and data contract +tooling all benefit from being able to describe a model in a standard, portable +way. + +JSON Schema is the standard for describing the shape of structured data. JSON +Schema can describe any sort of data, but in the context of HTTP APIs it's a +powerful way to describe types, constraints, enums, nested objects, arrays, +defaults, and examples. + +OpenAPI is an API description format which describes the entire surface of a +HTTP API. Since OpenAPI 3.1, the "Schema Object" is explicitly a JSON Schema +vocabulary, which means it inherits the JSON Schema keywords and semantics. + +Putting all of that together, it;s possible to turn Pydantic models into JSON +Schema, inspect the JSON Schema they generate, and convert that schema +information into an OpenAPI document suitable for SDK generation and API +tooling. ## Prerequisites -Before we get started, make sure you have [Python](https://www.python.org/downloads/) 3.10 or higher installed on your machine. Check your Python version by running the following command: +Python 3.9 or higher is required. Run the following command to confirm the +Python version: ```bash filename="Terminal" python --version ``` -We use Python 3.13.3 in this guide, but any version of Python 3.10 or higher should work. +This guide uses Python 3.14.7, though any version of Python 3.9 or higher works. ## Creating a new Python project @@ -39,107 +72,177 @@ source venv/bin/activate ## Install the required libraries -We'll install Pydantic and PyYAML to generate and pretty-print the OpenAPI document: +Pydantic, FastAPI, and PyYAML support schema generation and OpenAPI document +formatting: ```bash filename="Terminal" # Install the Pydantic library -pip install pydantic +pip install "pydantic>=2.13.4,<3" # Install the PyYAML library for pretty-printing the OpenAPI schema -pip install pyyaml +pip install "pyyaml>=6.0.3" + +# Install FastAPI for the API +pip install "fastapi>=0.115.6" ``` ## Pydantic to OpenAPI document walkthrough -Let's follow a step-by-step process to generate an OpenAPI document from a Pydantic model without any additional libraries. +The following steps generate an OpenAPI document from a Pydantic model without +additional libraries and improve the result in stages. -### Defining a simple Pydantic model +## Generating the first OpenAPI document -Create a new Python file called `models.py` and define a simple Pydantic model. +The sample app in [examples/framework-pydantic](https://github.com/speakeasy-api/examples/tree/main/examples/framework-pydantic) exposes a minimal train travel API with stations, trips, and bookings. -In this example, we define a Pydantic model called `Pet` with three fields: `id`, `name`, and `breed`. The `id` field is an integer, and the `name` and `breed` fields are strings. +```bash filename="Terminal" +cd examples/framework-pydantic +python -m venv .venv +source .venv/bin/activate +pip install -e . -```python -from pydantic import BaseModel +python generate_openapi.py +``` + +The generated document is intentionally basic at this stage: the endpoints +exist, but the schema metadata is still minimal. + +## Previewing the OpenAPI document + +The team at [Scalar](https://scalar.com/) has built a CLI and documentation +tool that can serve an OpenAPI document as a web app with a three-column API +documentation layout. +Generate and preview the latest OpenAPI document: -class Pet(BaseModel): - id: int - name: str - breed: str +```bash filename="Terminal" +python generate_openapi.py +npx @scalar/cli document serve openapi.yaml ``` -### Generating a JSON schema for the Pydantic model +![Scalar UI](/assets/openapi/pydantic/scalar.png) + +The API routes are listed in the navigation pane on the left. Select +**`/bookings` POST** to inspect one of the Train Travel API operations: + +![POST request API endpoint](/assets/openapi/pydantic/scalar-post.png) -Add a new function called `print_json_schema` to the `models.py` file that prints the JSON schema for the `Pet` model. +As the OpenAPI is improved throughout this guide, the preview can be run again to see how it's improving. -This function uses the `model_json_schema` method provided by Pydantic to generate the JSON schema, which Python then prints to the console as YAML. We use YAML for readability, but the output is still a valid JSON schema. +### Defining a simple Pydantic model + +Create a new Python file called `models.py` and define a simple model. This +example starts with a `Station` model, a straightforward schema that becomes a +useful OpenAPI object. + +```python +from pydantic import BaseModel, Field + +class Station(BaseModel): + id: str = Field(..., description="Unique station ID.", examples=["efdbb9d1-02c2-4bc3-afb7-6788d8782b1e"]) + name: str = Field(..., description="Station name.", examples=["Berlin Hauptbahnhof"]) + country_code: str = Field(..., description="ISO 3166-1 alpha-2 country code.", examples=["DE"]) + timezone: str = Field(..., description="IANA timezone of the station.", examples=["Europe/Berlin"]) +``` + +### Generating JSON Schema for the Pydantic model + +Add a function called `print_json_schema` to inspect the generated schema before +expanding the sample app further. + +This function uses the `model_json_schema` method provided by Pydantic to +generate the schema and print it as YAML for readability. ```python import yaml -from pydantic import BaseModel +from pydantic import BaseModel, Field -class Pet(BaseModel): - id: int - name: str - breed: str +class Station(BaseModel): + id: str = Field(..., description="Unique station ID.", examples=["efdbb9d1-02c2-4bc3-afb7-6788d8782b1e"]) + name: str = Field(..., description="Station name.", examples=["Berlin Hauptbahnhof"]) + country_code: str = Field(..., description="ISO 3166-1 alpha-2 country code.", examples=["DE"]) + timezone: str = Field(..., description="IANA timezone of the station.", examples=["Europe/Berlin"]) def print_json_schema(): - print(yaml.dump(Pet.model_json_schema())) + print(yaml.dump(Station.model_json_schema())) if __name__ == "__main__": print_json_schema() ``` -Run `python models.py` to generate the JSON schema for the `Pet` model and print it as YAML: +Running `python models.py` generates the JSON schema for the train station model: ```yaml properties: - breed: - title: Breed + country_code: + description: ISO 3166-1 alpha-2 country code. + examples: + - DE + title: Country Code type: string id: + description: Unique station ID. + examples: + - efdbb9d1-02c2-4bc3-afb7-6788d8782b1e title: Id - type: integer + type: string name: + description: Station name. + examples: + - Berlin Hauptbahnhof title: Name type: string + timezone: + description: IANA timezone of the station. + examples: + - Europe/Berlin + title: Timezone + type: string required: - id - name - - breed -title: Pet + - country_code + - timezone +title: Station type: object ``` -### Multiple Pydantic models +This is the core workflow for the rest of the article: define Pydantic models, +inspect the JSON Schema, and convert that schema into an OpenAPI document for a +train travel API. -Let's add another Pydantic model called `Owner` to the `models.py` file. +### Multiple Pydantic models -The `Owner` model has two fields: `id` and `name`. Both fields are integers. Additionally, the `Owner` model has a list of `Pet` objects. +Let's add a second model called `Trip` to the `models.py` file. A trip +references one station as the origin and another as the destination, which is a +useful pattern for real-world rail data. ```python import yaml -from pydantic import BaseModel +from pydantic import BaseModel, Field -class Pet(BaseModel): - id: int - name: str - breed: str +class Station(BaseModel): + id: str = Field(..., description="Unique station ID.") + name: str = Field(..., description="Station name.") + country_code: str = Field(..., description="ISO 3166-1 alpha-2 country code.") + timezone: str = Field(..., description="IANA timezone of the station.") -class Owner(BaseModel): - id: int - name: str - pets: list[Pet] +class Trip(BaseModel): + id: str = Field(..., description="Unique trip ID.") + origin: str = Field(..., description="Origin station ID.") + destination: str = Field(..., description="Destination station ID.") + departure_time: str = Field(..., description="Departure time in ISO 8601 format.") + arrival_time: str = Field(..., description="Arrival time in ISO 8601 format.") + price: float = Field(..., description="Trip price in EUR.") def print_json_schema(): - print(yaml.dump(Pet.model_json_schema())) + print(yaml.dump(Station.model_json_schema())) if __name__ == "__main__": @@ -148,26 +251,29 @@ if __name__ == "__main__": ### Generating a JSON schema for multiple Pydantic models -Update the `print_json_schema` function to print the JSON schema for both the `Pet` and `Owner` models. +Update the `print_json_schema` function to print the JSON schema for both the `Station` and `Trip` models. -Note that we're now calling the [`models_json_schema`](https://docs.pydantic.dev/2.7/api/json_schema/#pydantic.json_schema.models_json_schema) function from `pydantic.json_schema` instead of the `model_json_schema` method. +The [`models_json_schema`](https://docs.pydantic.dev/latest/api/json_schema/#pydantic.json_schema.models_json_schema) function from `pydantic.json_schema` replaces the `model_json_schema` method for multi-model output. ```python import yaml -from pydantic import BaseModel +from pydantic import BaseModel, Field from pydantic.json_schema import models_json_schema - -class Pet(BaseModel): - id: int - name: str - breed: str +class Station(BaseModel): + id: str = Field(..., description="Unique station ID.") + name: str = Field(..., description="Station name.") + country_code: str = Field(..., description="ISO 3166-1 alpha-2 country code.") + timezone: str = Field(..., description="IANA timezone of the station.") -class Owner(BaseModel): - id: int - name: str - pets: list[Pet] +class Trip(BaseModel): + id: str = Field(..., description="Unique trip ID.") + origin: str = Field(..., description="Origin station ID.") + destination: str = Field(..., description="Destination station ID.") + departure_time: str = Field(..., description="Departure time in ISO 8601 format.") + arrival_time: str = Field(..., description="Arrival time in ISO 8601 format.") + price: float = Field(..., description="Trip price in EUR.") def print_json_schema(models): @@ -178,77 +284,98 @@ def print_json_schema(models): if __name__ == "__main__": - print_json_schema([Pet, Owner]) + print_json_schema([Station, Trip]) ``` -Run `python models.py` to generate the JSON schema for both the `Pet` and `Owner` models and print it as YAML: +Run `python models.py` to generate the JSON schema for both the `Station` and +`Trip` models and print it as YAML: ```yaml $defs: - Owner: + Station: properties: + country_code: + title: Country Code + type: string id: title: Id - type: integer + type: string name: title: Name type: string - pets: - items: - $ref: "#/$defs/Pet" - title: Pets - type: array + timezone: + title: Timezone + type: string required: - id - name - - pets - title: Owner + - country_code + - timezone + title: Station type: object - Pet: + Trip: properties: - breed: - title: Breed + arrival_time: + title: Arrival Time + type: string + departure_time: + title: Departure Time + type: string + destination: + title: Destination type: string id: title: Id - type: integer - name: - title: Name type: string + origin: + title: Origin + type: string + price: + title: Price + type: number required: - id - - name - - breed - title: Pet + - origin + - destination + - departure_time + - arrival_time + - price + title: Trip type: object ``` -The generated schema includes definitions for both the `Pet` and `Owner` models. The `Owner` model has a reference to the `Pet` model, indicating that the `Owner` model contains a list of `Pet` objects. - -Note that the root of the schema includes a `$defs` key that contains the definitions for both models, and the `Owner` model references the `Pet` model using the `$ref` keyword. +The generated schema includes definitions for both the `Station` and `Trip` +models. The `Trip` model captures the route information that a train travel API needs +for timetables and pricing. ### Customizing Pydantic JSON schema generation -Let's customize the generated JSON schema to reference the `Pet` model using the `#/components/schemas` path instead of `$defs`. +Let's customize the generated JSON schema to reference the `Station` model using +the `#/components/schemas` path instead of `$defs`. -We'll use the `ref_template` parameter of the `models_json_schema` function to specify the reference template. +The `ref_template` parameter of the `models_json_schema` function specifies the +reference template. ```python import yaml -from pydantic import BaseModel +from pydantic import BaseModel, Field from pydantic.json_schema import models_json_schema -class Pet(BaseModel): - id: int - name: str - breed: str +class Station(BaseModel): + id: str = Field(..., description="Unique station ID.") + name: str = Field(..., description="Station name.") + country_code: str = Field(..., description="ISO 3166-1 alpha-2 country code.") + timezone: str = Field(..., description="IANA timezone of the station.") -class Owner(BaseModel): - id: int - name: str - pets: list[Pet] +class Trip(BaseModel): + id: str = Field(..., description="Unique trip ID.") + origin: str = Field(..., description="Origin station ID.") + destination: str = Field(..., description="Destination station ID.") + departure_time: str = Field(..., description="Departure time in ISO 8601 format.") + arrival_time: str = Field(..., description="Arrival time in ISO 8601 format.") + price: float = Field(..., description="Trip price in EUR.") def print_json_schema(models): @@ -260,27 +387,32 @@ def print_json_schema(models): if __name__ == "__main__": - print_json_schema([Pet, Owner]) + print_json_schema([Station, Trip]) ``` -Next, we'll update the `print_json_schema` function to print a JSON schema that resembles an OpenAPI document's `components` section. +The next step updates the `print_json_schema` function to print a JSON schema +that resembles an OpenAPI document's `components` section. ```python import yaml -from pydantic import BaseModel +from pydantic import BaseModel, Field from pydantic.json_schema import models_json_schema -class Pet(BaseModel): - id: int - name: str - breed: str +class Station(BaseModel): + id: str = Field(..., description="Unique station ID.") + name: str = Field(..., description="Station name.") + country_code: str = Field(..., description="ISO 3166-1 alpha-2 country code.") + timezone: str = Field(..., description="IANA timezone of the station.") -class Owner(BaseModel): - id: int - name: str - pets: list[Pet] +class Trip(BaseModel): + id: str = Field(..., description="Unique trip ID.") + origin: str = Field(..., description="Origin station ID.") + destination: str = Field(..., description="Destination station ID.") + departure_time: str = Field(..., description="Departure time in ISO 8601 format.") + arrival_time: str = Field(..., description="Arrival time in ISO 8601 format.") + price: float = Field(..., description="Trip price in EUR.") def print_json_schema(models): @@ -290,81 +422,102 @@ def print_json_schema(models): ) openapi_schema = { "components": { - "schemas": schemas.get('$defs'), + "schemas": schemas.get("$defs"), } } print(yaml.dump(openapi_schema)) if __name__ == "__main__": - print_json_schema([Pet, Owner]) + print_json_schema([Station, Trip]) ``` -Run `python models.py` to generate the OpenAPI document for both the `Pet` and `Owner` models. +Run `python models.py` to generate the OpenAPI document for both the `Station` and `Trip` models. -The generated OpenAPI document includes the `components` section, with definitions for both the `Pet` and `Owner` models. +The generated OpenAPI document includes the `components` section, with +definitions for both the `Station` and `Trip` models. ```yaml components: schemas: - Owner: + Station: properties: + country_code: + title: Country Code + type: string id: title: Id - type: integer + type: string name: title: Name type: string - pets: - items: - $ref: "#/components/schemas/Pet" - title: Pets - type: array + timezone: + title: Timezone + type: string required: - id - name - - pets - title: Owner + - country_code + - timezone + title: Station type: object - Pet: + Trip: properties: - breed: - title: Breed + arrival_time: + title: Arrival Time + type: string + departure_time: + title: Departure Time + type: string + destination: + title: Destination type: string id: title: Id - type: integer - name: - title: Name type: string + origin: + title: Origin + type: string + price: + title: Price + type: number required: - id - - name - - breed - title: Pet + - origin + - destination + - departure_time + - arrival_time + - price + title: Trip type: object ``` -The JSON schema we generated resembles an OpenAPI document's `components` section, but to generate a valid OpenAPI document, we need to add the `openapi` and `info` sections. +The generated JSON schema resembles an OpenAPI document's `components` section, +but a valid OpenAPI document also needs the `openapi` and `info` sections. -Edit the `print_json_schema` function in `models.py` to include the `openapi` and `info` sections in the generated OpenAPI document. +Edit the `print_json_schema` function in `models.py` to include the `openapi` +and `info` sections in the generated OpenAPI document. ```python import yaml -from pydantic import BaseModel +from pydantic import BaseModel, Field from pydantic.json_schema import models_json_schema -class Pet(BaseModel): - id: int - name: str - breed: str +class Station(BaseModel): + id: str = Field(..., description="Unique station ID.") + name: str = Field(..., description="Station name.") + country_code: str = Field(..., description="ISO 3166-1 alpha-2 country code.") + timezone: str = Field(..., description="IANA timezone of the station.") -class Owner(BaseModel): - id: int - name: str - pets: list[Pet] +class Trip(BaseModel): + id: str = Field(..., description="Unique trip ID.") + origin: str = Field(..., description="Origin station ID.") + destination: str = Field(..., description="Destination station ID.") + departure_time: str = Field(..., description="Departure time in ISO 8601 format.") + arrival_time: str = Field(..., description="Arrival time in ISO 8601 format.") + price: float = Field(..., description="Trip price in EUR.") def print_json_schema(models): @@ -375,105 +528,68 @@ def print_json_schema(models): openapi_schema = { "openapi": "3.1.0", "info": { - "title": "Pet Sitter API", + "title": "Train Travel API", "version": "0.0.1", }, "components": { - "schemas": schemas.get('$defs'), - } + "schemas": schemas.get("$defs"), + }, } - print(yaml.dump(openapi_schema)) + print(yaml.dump(openapi_schema, sort_keys=False)) if __name__ == "__main__": - print_json_schema([Pet, Owner]) + print_json_schema([Station, Trip]) ``` -Run `python models.py` to generate the complete OpenAPI document for both the `Pet` and `Owner` models. +Run `python models.py` to generate the complete OpenAPI document for both the +`Station` and `Trip` models. -The generated OpenAPI document includes the `openapi`, `info`, and `components` sections with definitions for both the `Pet` and `Owner` models. +The generated OpenAPI document includes the `openapi`, `info`, and `components` +sections with definitions for both the `Station` and `Trip` models. -```yaml -openapi: 3.1.0 -info: - title: Pet Sitter API - version: 0.0.1 -components: - schemas: - Owner: - properties: - id: - title: Id - type: integer - name: - title: Name - type: string - pets: - items: - $ref: "#/components/schemas/Pet" - title: Pets - type: array - required: - - id - - name - - pets - title: Owner - type: object - Pet: - properties: - id: - title: Id - type: integer - name: - title: Name - type: string - breed: - title: Breed - type: string - required: - - id - - name - - breed - title: Pet - type: object -``` - -Now we have a complete OpenAPI document that we can use to generate SDK clients for our API. However, the generated OpenAPI document does not contain descriptions or example values for the models. We can add these details to the Pydantic models to improve the generated OpenAPI document. +A complete OpenAPI document now exists for SDK generation. The generated +document does not yet include descriptions or example values for the models, +though those details can be added directly to the Pydantic models to improve the +resulting OpenAPI output. ### Adding descriptions to Pydantic models -Let's add docstrings to the `Pet` and `Owner` models to include additional information in the generated OpenAPI document. +Let's add docstrings to the `Station` and `Trip` models to include additional +information in the generated OpenAPI document. ```python import yaml -from pydantic import BaseModel +from pydantic import BaseModel, Field from pydantic.json_schema import models_json_schema -class Pet(BaseModel): +class Station(BaseModel): """ - A Pet in the system. + A train station in the network. - ID is unique. - Can have multiple owners. + Every station is uniquely identified and belongs to a country. """ - id: int - name: str - breed: str + id: str = Field(..., description="Unique station ID.") + name: str = Field(..., description="Station name.") + country_code: str = Field(..., description="ISO 3166-1 alpha-2 country code.") + timezone: str = Field(..., description="IANA timezone of the station.") -class Owner(BaseModel): +class Trip(BaseModel): """ - An Owner of Pets in the system. + A scheduled train trip between two stations. - ID is unique. - Can have multiple pets. + Includes departure and arrival times, and the trip fare. """ - id: int - name: str - pets: list[Pet] + id: str = Field(..., description="Unique trip ID.") + origin: str = Field(..., description="Origin station ID.") + destination: str = Field(..., description="Destination station ID.") + departure_time: str = Field(..., description="Departure time in ISO 8601 format.") + arrival_time: str = Field(..., description="Arrival time in ISO 8601 format.") + price: float = Field(..., description="Trip price in EUR.") def print_json_schema(models): @@ -484,7 +600,7 @@ def print_json_schema(models): openapi_schema = { "openapi": "3.1.0", "info": { - "title": "Pet Sitter API", + "title": "Train Travel API", "version": "0.0.1", }, "components": { @@ -495,75 +611,18 @@ def print_json_schema(models): if __name__ == "__main__": - print_json_schema([Pet, Owner]) + print_json_schema([Station, Trip]) ``` -If we run `python models.py`, we see that our `Owner` schema now includes a description field, derived from the docstring we added to the `Owner` Pydantic model. - -```yaml -openapi: 3.1.0 -info: - title: Pet Sitter API - version: 0.0.1 -components: - schemas: - Owner: - description: "An Owner of Pets in the system. - - - ID is unique. - - Can have multiple pets." - properties: - id: - title: Id - type: integer - name: - title: Name - type: string - pets: - items: - $ref: "#/components/schemas/Pet" - title: Pets - type: array - required: - - id - - name - - pets - title: Owner - type: object - Pet: - description: "A Pet in the system. - - - ID is unique. - - Can have multiple owners." - properties: - id: - title: Id - type: integer - name: - title: Name - type: string - breed: - title: Breed - type: string - required: - - id - - name - - breed - title: Pet - type: object -``` - -The `Pet` schema now also includes a description field, derived from the docstring we added to the `Pet` Pydantic model. +Running `python models.py` shows a `Trip` schema description derived from the +docstring added to the `Trip` model. ### Adding OpenAPI titles and descriptions to Pydantic fields -Let's add titles and descriptions to the fields of the `Pet` and `Owner` models to include additional information in the generated OpenAPI document. +Let's add titles and descriptions to the fields of the `Station` and `Trip` +models to provide richer schema metadata. -We'll use the `Field` class from Pydantic to add descriptions to the fields. +The `Field` class from Pydantic adds descriptions to the fields. ```python import yaml @@ -571,32 +630,20 @@ from pydantic import BaseModel, Field from pydantic.json_schema import models_json_schema -class Pet(BaseModel): - """ - A Pet in the system. - - ID is unique. - Can have multiple owners. - """ - - id: int = Field(..., title="Pet ID", description="The pet's unique identifier") - name: str = Field(..., title="Pet Name", description="Name of the pet") - breed: str = Field(..., title="Pet Breed", description="Breed of the pet") - - -class Owner(BaseModel): - """ - An Owner of Pets in the system. +class Station(BaseModel): + id: str = Field(..., title="Station ID", description="Unique station ID.") + name: str = Field(..., title="Station Name", description="Station name.") + country_code: str = Field(..., title="Country Code", description="ISO 3166-1 alpha-2 country code.") + timezone: str = Field(..., title="Timezone", description="IANA timezone of the station.") - ID is unique. - Can have multiple pets. - """ - id: int = Field(..., title="Owner ID", description="Owner's unique identifier") - name: str = Field(..., title="Owner Name", description="The owner's full name") - pets: list[Pet] = Field( - ..., title="Owner's Pets", description="The pets that belong to this owner" - ) +class Trip(BaseModel): + id: str = Field(..., title="Trip ID", description="Unique trip ID.") + origin: str = Field(..., title="Origin", description="Origin station ID.") + destination: str = Field(..., title="Destination", description="Destination station ID.") + departure_time: str = Field(..., title="Departure Time", description="Departure time in ISO 8601 format.") + arrival_time: str = Field(..., title="Arrival Time", description="Arrival time in ISO 8601 format.") + price: float = Field(..., title="Price", description="Trip price in EUR.") def print_json_schema(models): @@ -607,7 +654,7 @@ def print_json_schema(models): openapi_schema = { "openapi": "3.1.0", "info": { - "title": "Pet Sitter API", + "title": "Train Travel API", "version": "0.0.1", }, "components": { @@ -618,81 +665,18 @@ def print_json_schema(models): if __name__ == "__main__": - print_json_schema([Pet, Owner]) + print_json_schema([Station, Trip]) ``` -If we run `python models.py`, we see that our `Pet` schema now includes descriptions for each field. - -```yaml -openapi: 3.1.0 -info: - title: Pet Sitter API - version: 0.0.1 -components: - schemas: - Owner: - description: "An Owner of Pets in the system. - - - ID is unique. - - Can have multiple pets." - properties: - id: - description: Owner's unique identifier - title: Owner ID - type: integer - name: - description: The owner's full name - title: Owner Name - type: string - pets: - description: The pets that belong to this owner - items: - $ref: "#/components/schemas/Pet" - title: Owner's Pets - type: array - required: - - id - - name - - pets - title: Owner - type: object - Pet: - description: "A Pet in the system. - - - ID is unique. - - Can have multiple owners." - properties: - id: - description: The pet's unique identifier - title: Pet ID - type: integer - name: - description: Name of the pet - title: Pet Name - type: string - breed: - description: Breed of the pet - title: Pet Breed - type: string - required: - - id - - name - - breed - title: Pet - type: object -``` +Running `python models.py` shows a `Trip` schema with descriptions for each field. ### Adding OpenAPI example values to Pydantic models -Examples help API users understand your API's data structures, and some SDK and documentation generators use OpenAPI example values to generate useful code snippets and documentation. +Examples help API users understand data structures. For a train API, example +values make station and trip records much easier to interpret. -Let's add example values to the `Pet` and `Owner` Pydantic models. Once again, we'll use the `Field` class from Pydantic to add example values to the fields. - -Note that the examples are added as a list per field, using the `examples` parameter. +The following example adds example values to the `Station` and `Trip` Pydantic +models by using the `Field` class from Pydantic. ```python import yaml @@ -700,60 +684,20 @@ from pydantic import BaseModel, Field from pydantic.json_schema import models_json_schema -class Pet(BaseModel): - """ - A Pet in the system. - - ID is unique. - Can have multiple owners. - """ - - id: int = Field( - ..., - title="Pet ID", - description="The pet's unique identifier", - examples=[1], - ) - name: str = Field( - ..., - title="Pet Name", - description="Name of the pet", - examples=["Fido"], - ) - breed: str = Field( - ..., - title="Pet Breed", - description="Breed of the pet", - examples=["Golden Retriever", "Siamese", "Parakeet"], - ) - - -class Owner(BaseModel): - """ - An Owner of Pets in the system. +class Station(BaseModel): + id: str = Field(..., description="Unique station ID.", examples=["ber-001"]) + name: str = Field(..., description="Station name.", examples=["Berlin Hauptbahnhof"]) + country_code: str = Field(..., description="ISO 3166-1 alpha-2 country code.", examples=["DE"]) + timezone: str = Field(..., description="IANA timezone of the station.", examples=["Europe/Berlin"]) - ID is unique. - Can have multiple pets. - """ - id: int = Field( - ..., - title="Owner ID", - description="Owner's unique identifier", - examples=[1], - ) - name: str = Field( - ..., - title="Owner Name", - description="The owner's full name", - examples=["John Doe"], - ) - pets: list[Pet] = Field( - ..., - title="Owner's Pets", - description="The pets that belong to this owner", - examples=[{"id": 1}], - ) +class Trip(BaseModel): + id: str = Field(..., description="Unique trip ID.", examples=["trip_001"]) + origin: str = Field(..., description="Origin station ID.", examples=["ber-001"]) + destination: str = Field(..., description="Destination station ID.", examples=["muc-017"]) + departure_time: str = Field(..., description="Departure time in ISO 8601 format.", examples=["2026-08-24T08:15:00Z"]) + arrival_time: str = Field(..., description="Arrival time in ISO 8601 format.", examples=["2026-08-24T10:05:00Z"]) + price: float = Field(..., description="Trip price in EUR.", examples=[89.0]) def print_json_schema(models): @@ -764,7 +708,7 @@ def print_json_schema(models): openapi_schema = { "openapi": "3.1.0", "info": { - "title": "Pet Sitter API", + "title": "Train Travel API", "version": "0.0.1", }, "components": { @@ -775,93 +719,16 @@ def print_json_schema(models): if __name__ == "__main__": - print_json_schema([Pet, Owner]) -``` - -If we run `python models.py`, we see that our `Pet` schema now includes example values for each field. - -```yaml -openapi: 3.1.0 -info: - title: Pet Sitter API - version: 0.0.1 -components: - schemas: - Owner: - description: "An Owner of Pets in the system. - - - ID is unique. - - Can have multiple pets." - properties: - id: - description: Owner's unique identifier - examples: - - 1 - title: Owner ID - type: integer - name: - description: The owner's full name - examples: - - John Doe - title: Owner Name - type: string - pets: - description: The pets that belong to this owner - examples: - - id: 1 - items: - $ref: "#/components/schemas/Pet" - title: Owner's Pets - type: array - required: - - id - - name - - pets - title: Owner - type: object - Pet: - description: "A Pet in the system. - - - ID is unique. - - Can have multiple owners." - properties: - id: - description: The pet's unique identifier - examples: - - 1 - title: Pet ID - type: integer - name: - description: Name of the pet - examples: - - Fido - title: Pet Name - type: string - breed: - description: Breed of the pet - examples: - - Golden Retriever - - Siamese - - Parakeet - title: Pet Breed - type: string - required: - - id - - name - - breed - title: Pet - type: object + print_json_schema([Station, Trip]) ``` ### Marking fields as optional in Pydantic models -By default, Pydantic marks all fields as required. You can mark a field as optional by setting the `default` parameter to `None`. +Pydantic marks all fields as required by default. A field becomes optional when +the `default` parameter is set to `None`. -Let's mark the `breed` field in the `Pet` model as optional by setting the `default` parameter to `None`. +The `timezone` field in the `Station` model becomes optional in the schema with +a default value of `None`. ```python import yaml @@ -869,60 +736,20 @@ from pydantic import BaseModel, Field from pydantic.json_schema import models_json_schema -class Pet(BaseModel): - """ - A Pet in the system. - - ID is unique. - Can have multiple owners. - """ - - id: int = Field( - ..., - title="Pet ID", - description="The pet's unique identifier", - examples=[1], - ) - name: str = Field( - ..., - title="Pet Name", - description="Name of the pet", - examples=["Fido"], - ) - breed: str | None = Field( - None, - title="Pet Breed", - description="Breed of the pet", - examples=["Golden Retriever", "Siamese", "Parakeet"], - ) +class Station(BaseModel): + id: str = Field(..., description="Unique station ID.", examples=["ber-001"]) + name: str = Field(..., description="Station name.", examples=["Berlin Hauptbahnhof"]) + country_code: str = Field(..., description="ISO 3166-1 alpha-2 country code.", examples=["DE"]) + timezone: str | None = Field(None, description="IANA timezone of the station.", examples=["Europe/Berlin"]) -class Owner(BaseModel): - """ - An Owner of Pets in the system. - - ID is unique. - Can have multiple pets. - """ - - id: int = Field( - ..., - title="Owner ID", - description="Owner's unique identifier", - examples=[1], - ) - name: str = Field( - ..., - title="Owner Name", - description="The owner's full name", - examples=["John Doe"], - ) - pets: list[Pet] = Field( - ..., - title="Owner's Pets", - description="The pets that belong to this owner", - examples=[{"id": 1}], - ) +class Trip(BaseModel): + id: str = Field(..., description="Unique trip ID.", examples=["trip_001"]) + origin: str = Field(..., description="Origin station ID.", examples=["ber-001"]) + destination: str = Field(..., description="Destination station ID.", examples=["muc-017"]) + departure_time: str = Field(..., description="Departure time in ISO 8601 format.", examples=["2026-08-24T08:15:00Z"]) + arrival_time: str = Field(..., description="Arrival time in ISO 8601 format.", examples=["2026-08-24T10:05:00Z"]) + price: float = Field(..., description="Trip price in EUR.", examples=[89.0]) def print_json_schema(models): @@ -933,7 +760,7 @@ def print_json_schema(models): openapi_schema = { "openapi": "3.1.0", "info": { - "title": "Pet Sitter API", + "title": "Train Travel API", "version": "0.0.1", }, "components": { @@ -944,95 +771,13 @@ def print_json_schema(models): if __name__ == "__main__": - print_json_schema([Pet, Owner]) -``` - -If we run `python models.py`, we see that the `breed` field in the `Pet` schema now has two types: `string` and `null`, and it has been removed from the `required` list. Only `id` and `name` are required fields after marking `breed` as optional. - -```yaml -openapi: 3.1.0 -info: - title: Pet Sitter API - version: 0.0.1 -components: - schemas: - Owner: - description: "An Owner of Pets in the system. - - - ID is unique. - - Can have multiple pets." - properties: - id: - description: Owner's unique identifier - examples: - - 1 - title: Owner ID - type: integer - name: - description: The owner's full name - examples: - - John Doe - title: Owner Name - type: string - pets: - description: The pets that belong to this owner - examples: - - id: 1 - items: - $ref: "#/components/schemas/Pet" - title: Owner's Pets - type: array - required: - - id - - name - - pets - title: Owner - type: object - Pet: - description: "A Pet in the system. - - - ID is unique. - - Can have multiple owners." - properties: - id: - description: The pet's unique identifier - examples: - - 1 - title: Pet ID - type: integer - name: - description: Name of the pet - examples: - - Fido - title: Pet Name - type: string - breed: - anyOf: - - type: string - - type: "null" - default: null - description: Breed of the pet - examples: - - Golden Retriever - - Siamese - - Parakeet - title: Pet Breed - required: - - id - - name - title: Pet - type: object + print_json_schema([Station, Trip]) ``` ### Adding enums to OpenAPI using Pydantic models Enums in OpenAPI are useful for defining a set of possible values for a field. - -Let's add an enum called `PetType` to the `Pet` model to represent different types of pets. +For a train travel API, a `BookingStatus` enum is a natural fit. ```python from enum import StrEnum @@ -1041,76 +786,33 @@ from pydantic import BaseModel, Field from pydantic.json_schema import models_json_schema -class PetType(StrEnum): - """ - An enumeration of pet types. - """ - - DOG = "dog" - CAT = "cat" - BIRD = "bird" - - -class Pet(BaseModel): - """ - A Pet in the system. +class BookingStatus(StrEnum): + CONFIRMED = "confirmed" + PENDING = "pending" + CANCELLED = "cancelled" - ID is unique. - Can have multiple owners. - """ - pet_type: PetType = Field( - ..., - title="Pet Type", - description="Type of pet", - examples=["dog", "cat", "bird"], - ) - id: int = Field( - ..., - title="Pet ID", - description="The pet's unique identifier", - examples=[1], - ) - name: str = Field( - ..., - title="Pet Name", - description="Name of the pet", - examples=["Fido"], - ) - breed: str | None = Field( - None, - title="Pet Breed", - description="Breed of the pet", - examples=["Golden Retriever", "Siamese", "Parakeet"], - ) +class Station(BaseModel): + id: str = Field(..., description="Unique station ID.", examples=["ber-001"]) + name: str = Field(..., description="Station name.", examples=["Berlin Hauptbahnhof"]) + country_code: str = Field(..., description="ISO 3166-1 alpha-2 country code.", examples=["DE"]) + timezone: str | None = Field(None, description="IANA timezone of the station.", examples=["Europe/Berlin"]) -class Owner(BaseModel): - """ - An Owner of Pets in the system. +class Trip(BaseModel): + id: str = Field(..., description="Unique trip ID.", examples=["trip_001"]) + origin: str = Field(..., description="Origin station ID.", examples=["ber-001"]) + destination: str = Field(..., description="Destination station ID.", examples=["muc-017"]) + departure_time: str = Field(..., description="Departure time in ISO 8601 format.", examples=["2026-08-24T08:15:00Z"]) + arrival_time: str = Field(..., description="Arrival time in ISO 8601 format.", examples=["2026-08-24T10:05:00Z"]) + price: float = Field(..., description="Trip price in EUR.", examples=[89.0]) - ID is unique. - Can have multiple pets. - """ - id: int = Field( - ..., - title="Owner ID", - description="Owner's unique identifier", - examples=[1], - ) - name: str = Field( - ..., - title="Owner Name", - description="The owner's full name", - examples=["John Doe"], - ) - pets: list[Pet] = Field( - ..., - title="Owner's Pets", - description="The pets that belong to this owner", - examples=[{"id": 1}], - ) +class Booking(BaseModel): + id: str = Field(..., description="Unique booking ID.", examples=["booking_123"]) + trip_id: str = Field(..., description="Trip ID for the selected route.", examples=["trip_001"]) + passenger_name: str = Field(..., description="Passenger name on the booking.", examples=["Ada Lovelace"]) + status: BookingStatus = Field(..., description="Current booking status.", examples=["confirmed"]) def print_json_schema(models): @@ -1121,7 +823,7 @@ def print_json_schema(models): openapi_schema = { "openapi": "3.1.0", "info": { - "title": "Pet Sitter API", + "title": "Train Travel API", "version": "0.0.1", }, "components": { @@ -1132,127 +834,37 @@ def print_json_schema(models): if __name__ == "__main__": - print_json_schema([Pet, Owner]) + print_json_schema([Station, Trip, Booking, BookingStatus]) ``` -In our generated OpenAPI document, we have a new `pet_type` field in the `Pet` schema. - -```yaml -openapi: 3.1.0 -info: - title: Pet Sitter API - version: 0.0.1 -components: - schemas: - Owner: - description: "An Owner of Pets in the system. - - - ID is unique. - - Can have multiple pets." - properties: - id: - description: Owner's unique identifier - examples: - - 1 - title: Owner ID - type: integer - name: - description: The owner's full name - examples: - - John Doe - title: Owner Name - type: string - pets: - description: The pets that belong to this owner - examples: - - id: 1 - items: - $ref: "#/components/schemas/Pet" - title: Owner's Pets - type: array - required: - - id - - name - - pets - title: Owner - type: object - Pet: - description: "A Pet in the system. - - - ID is unique. - - Can have multiple owners." - properties: - pet_type: - allOf: - - $ref: "#/components/schemas/PetType" - description: Type of pet - examples: - - dog - - cat - - bird - title: Pet Type - id: - description: The pet's unique identifier - examples: - - 1 - title: Pet ID - type: integer - name: - description: Name of the pet - examples: - - Fido - title: Pet Name - type: string - breed: - anyOf: - - type: string - - type: "null" - default: null - description: Breed of the pet - examples: - - Golden Retriever - - Siamese - - Parakeet - title: Pet Breed - required: - - pet_type - - id - - name - title: Pet - type: object - PetType: - description: An enumeration of pet types. - enum: - - dog - - cat - - bird - title: PetType - type: string -``` - -This enum is represented as a separate schema in the OpenAPI document. +This enum is represented as a separate schema in the OpenAPI document and makes +the booking state explicit to both SDK generators and API consumers. ## Adding paths and operations to the OpenAPI document -Now that we have generated an OpenAPI document from our Pydantic models, we can use the schema to generate SDK clients for our API. +A generated OpenAPI document from the Pydantic models is ready for SDK usage, +but it does not yet include the `paths` section that defines the API endpoints +and operations. -However, the OpenAPI document we generated, while valid, does not include the `paths` section, which defines the API endpoints and operations. +Pydantic with FastAPI supports direct endpoint and operation definition in a +FastAPI application. [FastAPI automatically generates the OpenAPI document for +the API](/openapi/frameworks/fastapi#speakeasy-integration), including the +`paths` section. -When using Pydantic with FastAPI, you can define your API endpoints and operations directly in your FastAPI application. [FastAPI automatically generates the OpenAPI document for your API](/openapi/frameworks/fastapi#speakeasy-integration), including the `paths` section. - -Let's see how we can define API endpoints and operations in a framework-agnostic way and add them to the OpenAPI document. +The next section defines API endpoints and operations in a framework-agnostic +way and adds them to the OpenAPI document. ### Installing openapi-pydantic -We'll use the [`openapi-pydantic`](https://github.com/mike-oakley/openapi-pydantic/) library to define a complete OpenAPI document with paths and operations. +The [`openapi-pydantic`](https://github.com/mike-oakley/openapi-pydantic/) +library defines a complete OpenAPI document with paths and operations. -The benefit of using `openapi-pydantic` is that it allows you to define the API endpoints and operations in a Python dictionary while still getting the benefit of Pydantic's IDE support and type checking. +The benefit of using `openapi-pydantic` is support for defining API endpoints +and operations in a Python dictionary while preserving Pydantic IDE support and +type checking. -The library includes convenience methods to convert Pydantic models to OpenAPI document components and to add them to the OpenAPI document. +The library includes convenience methods to convert Pydantic models to OpenAPI +document components and to add them to the OpenAPI document. Install the `openapi-pydantic` library: @@ -1262,7 +874,8 @@ pip install openapi-pydantic ### Defining API endpoints -Create a new file called `api.py` to define the API endpoints using the `openapi-pydantic` library: +Create a new file called `api.py` to define the API endpoints using the +`openapi-pydantic` library: ```python filename="api.py" from typing import List @@ -1270,93 +883,110 @@ import yaml from pydantic import BaseModel, Field from openapi_pydantic.v3 import OpenAPI, Info, PathItem, Operation from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class -from models import Pet, Owner +from models import Station, Trip, Booking # Define response wrapper models -class PetsResponse(BaseModel): - """A response containing a list of pets""" - pets: List[Pet] = Field(..., description="List of pets") +class StationsResponse(BaseModel): + """A response containing a list of stations.""" + stations: List[Station] = Field(..., description="List of train stations") + +class TripsResponse(BaseModel): + """A response containing a list of trips.""" + trips: List[Trip] = Field(..., description="List of train trips") -class OwnersResponse(BaseModel): - """A response containing a list of owners""" - owners: List[Owner] = Field(..., description="List of owners") +class BookingsResponse(BaseModel): + """A response containing a list of bookings.""" + bookings: List[Booking] = Field(..., description="List of bookings") def construct_base_open_api() -> OpenAPI: return OpenAPI( openapi="3.1.0", - info=Info(title="Pet Sitter API", version="0.0.1"), + info=Info(title="Train Travel API", version="0.0.1"), servers=[{"url": "http://127.0.0.1:4010", "description": "Local prism server"}], paths={ - # GET and POST endpoints for pets collection - "/pets": PathItem( + "/stations": PathItem( get=Operation( - operationId="listPets", - description="List all pets", + operationId="listStations", + description="List all stations", responses={ "200": { - "description": "A list of pets", + "description": "A list of stations", "content": { "application/json": { - "schema": PydanticSchema(schema_class=PetsResponse) + "schema": PydanticSchema(schema_class=StationsResponse) } }, } }, ), - post=Operation( - operationId="createPet", - description="Create a pet", - requestBody={ - "content": { - "application/json": {"schema": PydanticSchema(schema_class=Pet)} - } - }, + ), + "/trips": PathItem( + get=Operation( + operationId="listTrips", + description="List all trips", responses={ - "201": { - "description": "Pet created", + "200": { + "description": "A list of trips", "content": { - "application/json": {"schema": PydanticSchema(schema_class=Pet)} + "application/json": { + "schema": PydanticSchema(schema_class=TripsResponse) + } }, } }, ), ), - # GET endpoint for a specific pet by ID - "/pets/{pet_id}": PathItem( + "/trips/{trip_id}": PathItem( get=Operation( - operationId="getPetById", - description="Get a pet by ID", + operationId="getTripById", + description="Get a trip by ID", parameters=[ { - "name": "pet_id", + "name": "trip_id", "in": "path", - "description": "ID of pet to return", + "description": "ID of trip to return", "required": True, - "schema": {"type": "integer", "format": "int64"}, - "examples": {"1": {"value": 1}}, + "schema": {"type": "string"}, + "examples": {"trip_001": {"value": "trip_001"}}, }, ], responses={ "200": { - "description": "A pet", + "description": "A trip", "content": { - "application/json": {"schema": PydanticSchema(schema_class=Pet)} + "application/json": {"schema": PydanticSchema(schema_class=Trip)} }, } }, ), ), - # GET endpoint for owners collection - "/owners": PathItem( + "/bookings": PathItem( + post=Operation( + operationId="createBooking", + description="Create a booking", + requestBody={ + "content": { + "application/json": {"schema": PydanticSchema(schema_class=Booking)} + } + }, + responses={ + "201": { + "description": "Booking created", + "content": { + "application/json": {"schema": PydanticSchema(schema_class=Booking)} + }, + } + }, + ), get=Operation( - operationId="listOwners", - description="List all owners", + operationId="listBookings", + description="List all bookings", responses={ "200": { - "description": "A list of owners", + "description": "A list of bookings", "content": { "application/json": { - "schema": PydanticSchema(schema_class=OwnersResponse) + "schema": PydanticSchema(schema_class=BookingsResponse) } }, } @@ -1366,7 +996,6 @@ def construct_base_open_api() -> OpenAPI: }, ) -# Generate the complete OpenAPI document open_api = construct_base_open_api() open_api = construct_open_api_with_schema_class(open_api) @@ -1384,12 +1013,12 @@ if __name__ == "__main__": This code defines: -1. Response models that wrap our Pydantic models (like `PetsResponse`) for consistent API responses +1. Response models that wrap our Pydantic models for consistent API responses 2. A function that builds the OpenAPI document with four endpoints: - - `GET /pets`: Lists all pets - - `POST /pets`: Creates a new pet - - `GET /pets/{pet_id}`: Gets a pet by ID - - `GET /owners`: Lists all owners + - `GET /stations`: Lists all stations + - `GET /trips`: Lists available trips + - `GET /trips/{trip_id}`: Gets a trip by ID + - `GET /bookings` and `POST /bookings`: Lists and creates bookings 3. Each endpoint includes: - An `operationId` for SDK generation - A description of what the endpoint does @@ -1401,88 +1030,101 @@ When run, this generates an `openapi.yaml` file with the full API specification: ```yaml filename="openapi.yaml" openapi: 3.1.0 info: - title: Pet Sitter API + title: Train Travel API version: 0.0.1 servers: - url: http://127.0.0.1:4010 description: Local prism server paths: - /pets: + /stations: get: - description: List all pets - operationId: listPets + description: List all stations + operationId: listStations responses: "200": - description: A list of pets + description: A list of stations content: application/json: schema: - $ref: "#/components/schemas/PetsResponse" - post: - description: Create a pet - operationId: createPet - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/Pet" + $ref: "#/components/schemas/StationsResponse" + /trips: + get: + description: List all trips + operationId: listTrips responses: - "201": - description: Pet created + "200": + description: A list of trips content: application/json: schema: - $ref: "#/components/schemas/Pet" - /pets/{pet_id}: + $ref: "#/components/schemas/TripsResponse" + /trips/{trip_id}: get: - description: Get a pet by ID - operationId: getPetById + description: Get a trip by ID + operationId: getTripById parameters: - - name: pet_id + - name: trip_id in: path - description: ID of pet to return + description: ID of trip to return required: true schema: - type: integer - format: int64 + type: string examples: - "1": - value: 1 + trip_001: + value: trip_001 responses: "200": - description: A pet + description: A trip content: application/json: schema: - $ref: "#/components/schemas/Pet" - /owners: + $ref: "#/components/schemas/Trip" + /bookings: get: - description: List all owners - operationId: listOwners + description: List all bookings + operationId: listBookings responses: "200": - description: A list of owners + description: A list of bookings content: application/json: schema: - $ref: "#/components/schemas/OwnersResponse" + $ref: "#/components/schemas/BookingsResponse" + post: + description: Create a booking + operationId: createBooking + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Booking" + responses: + "201": + description: Booking created + content: + application/json: + schema: + $ref: "#/components/schemas/Booking" components: schemas: # Schemas for our models are included here - # (Pet, Owner, PetType, PetsResponse, OwnersResponse) + # (Station, Trip, Booking, BookingStatus, StationsResponse, TripsResponse, BookingsResponse) ``` -The generated OpenAPI document includes all the components from our Pydantic models, along with the API endpoints we defined. The schemas include all the titles, descriptions, examples, and other details we added to our Pydantic models. +The generated OpenAPI document includes all the components from the Pydantic +models along with the defined API endpoints. The schemas include titles, +descriptions, examples, and other details added to the Pydantic models. ## Generating an SDK from the OpenAPI document -Now that we have a complete OpenAPI document with paths and operations, we can use it to generate an SDK client for our API. +A complete OpenAPI document with paths and operations is ready for SDK generation. ### Prerequisites for SDK generation -Install Speakeasy by following the [Speakeasy installation instructions](/docs/speakeasy-reference/cli/getting-started#install) +Install Speakeasy by following the [Speakeasy installation +instructions](/docs/speakeasy-reference/cli/getting-started#install) -On macOS, you can install Speakeasy using Homebrew: +On macOS, Homebrew installs Speakeasy: ```bash filename="Terminal" brew install speakeasy-api/tap/speakeasy @@ -1502,33 +1144,51 @@ Run the following command to generate an SDK from the `openapi.yaml` file: speakeasy quickstart ``` -Follow the onscreen prompts to provide the necessary configuration details for your new SDK, such as the name, schema location, and output path. Enter `openapi.yaml` when prompted for the OpenAPI document location and select TypeScript when prompted for which language you would like to generate. +Use the onscreen prompts to provide the required SDK configuration details, +including the name, schema location, and output path. Enter `openapi.yaml` as +the OpenAPI document location and select TypeScript as the generation language. -Speakeasy [validates](/docs/sdks/core-concepts#validation) the OpenAPI document to check that it's ready for code generation. Validation issues will be printed in the terminal. The generated SDK will be saved as a folder in your project. +Speakeasy [validates](/docs/sdks/core-concepts#validation) the OpenAPI document +to confirm readiness for code generation. Validation issues appear in the +terminal, and the generated SDK saves as a folder in the project. -![Speakeasy quickstart command output](/assets/openapi/speakeasy-quickstart-output.png) +![Speakeasy quickstart command +output](/assets/openapi/speakeasy-quickstart-output.png) -Speakeasy also suggests improvements for your SDK using [Speakeasy Suggest](/docs/prep-openapi/maintenance), which is an AI-powered tool in Speakeasy Studio. You can view the suggestions in Speakeasy Studio: +Speakeasy also suggests improvements for the SDK using [Speakeasy +Suggest](/docs/prep-openapi/maintenance), an AI-powered tool in Speakeasy +Studio. Suggestions appear in Speakeasy Studio: -![Speakeasy Studio suggestions](/assets/openapi/hono/speakeasy-studio-suggestions.png) +![Speakeasy Studio +suggestions](/assets/openapi/hono/speakeasy-studio-suggestions.png) ### Adding Speakeasy extensions to the OpenAPI document -Speakeasy uses [OpenAPI extensions](/openapi/extensions) to provide additional information for generating SDKs. +Speakeasy uses [OpenAPI extensions](/openapi/extensions) to provide additional +information for generating SDKs. -We can add extensions using [OpenAPI overlays](/openapi/overlays), which are YAML files that [Speakeasy lays on top of the OpenAPI document](/docs/prep-openapi/overlays/create-overlays). +Extensions can be added with [OpenAPI overlays](/openapi/overlays), which are +YAML files that [Speakeasy lays on top of the OpenAPI +document](/docs/prep-openapi/overlays/create-overlays). -We can use overlays alongside [OpenAPI transformations](/docs/prep-openapi/transformations) to improve the OpenAPI document for SDK generation. +Overlays can be used alongside [OpenAPI +transformations](/docs/prep-openapi/transformations) to improve the OpenAPI +document for SDK generation. -Transformations are predefined functions that allow you to remove unused components, filter operations, and format your OpenAPI document. Unlike overlays, transformations directly modify the OpenAPI document itself. +Transformations are predefined functions that remove unused components, filter +operations, and format the OpenAPI document. Unlike overlays, transformations +directly modify the OpenAPI document itself. -Note that for Speakeasy OpenAPI extensions, you can also add extensions directly to the OpenAPI document using the `x-` prefix. +For Speakeasy OpenAPI extensions, add extensions directly to the OpenAPI +document using the `x-` prefix. -For example, you can add the [`x-speakeasy-retries`](/docs/customize/runtime/retries) extension to have Speakeasy generate retry logic in the SDK. +For example, add the [`x-speakeasy-retries`](/docs/customize/runtime/retries) +extension to enable retry logic in the generated SDK. -Import the `Dict` and `Any` types from the `typing` module in `api.py`, and `ConfigDict` from `pydantic`. +Import the `Dict` and `Any` types from the `typing` module in `api.py`, and +`ConfigDict` from `pydantic`. -We'll use these types to define the `x-speakeasy-retries` extension in the OpenAPI document. +These types define the `x-speakeasy-retries` extension in the OpenAPI document. ```python filename="api.py" from typing import List, Dict, Any @@ -1536,16 +1196,20 @@ import yaml from pydantic import BaseModel, Field, ConfigDict from openapi_pydantic.v3 import OpenAPI, Info, PathItem, Operation from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class -from models import Pet, Owner +from models import Station, Trip, Booking # Define response models -class PetsResponse(BaseModel): - """A response containing a list of pets""" - pets: List[Pet] = Field(..., description="List of pets") +class StationsResponse(BaseModel): + """A response containing a list of stations.""" + stations: List[Station] = Field(..., description="List of stations") + +class TripsResponse(BaseModel): + """A response containing a list of trips.""" + trips: List[Trip] = Field(..., description="List of trips") -class OwnersResponse(BaseModel): - """A response containing a list of owners""" - owners: List[Owner] = Field(..., description="List of owners") +class BookingsResponse(BaseModel): + """A response containing a list of bookings.""" + bookings: List[Booking] = Field(..., description="List of bookings") # Define OpenAPI class with retry extension class OpenAPIwithRetries(OpenAPI): @@ -1560,7 +1224,7 @@ class OpenAPIwithRetries(OpenAPI): def construct_base_open_api() -> OpenAPIwithRetries: return OpenAPIwithRetries( openapi="3.1.0", - info=Info(title="Pet Sitter API", version="0.0.1"), + info=Info(title="Train Travel API", version="0.0.1"), servers=[{"url": "http://127.0.0.1:4010", "description": "Local prism server"}], # Add retry configuration xSpeakeasyRetries={ @@ -1598,29 +1262,32 @@ x-speakeasy-retries: ### Adding tags to the OpenAPI document -To group operations in the OpenAPI document, you can use tags. This also allows Speakeasy to structure the generated SDK code and documentation logically. +Tags group operations in the OpenAPI document and help structure generated SDK +code and documentation logically. -Add a `tags` field to the `OpenAPIwithRetries` object, then add a `tags` field to each operation in the `construct_base_open_api` function: +Add a `tags` field to the `OpenAPIwithRetries` object, then add a `tags` field +to each operation in the `construct_base_open_api` function: ```python filename="api.py" def construct_base_open_api() -> OpenAPIwithRetries: return OpenAPIwithRetries( # Basic API info openapi="3.1.0", - info=Info(title="Pet Sitter API", version="0.0.1"), + info=Info(title="Train Travel API", version="0.0.1"), # Define tags for grouping operations tags=[ - {"name": "pets", "description": "Operations about pets"}, - {"name": "owners", "description": "Operations about owners"}, + {"name": "stations", "description": "Operations about stations"}, + {"name": "trips", "description": "Operations about trips"}, + {"name": "bookings", "description": "Operations about bookings"}, ], # API endpoints with tags applied paths={ - "/pets": PathItem( + "/stations": PathItem( get=Operation( - operationId="listPets", - tags=["pets"], + operationId="listStations", + tags=["stations"], # other properties... ), ), @@ -1636,24 +1303,29 @@ python api.py speakeasy quickstart ``` -Speakeasy will detect the changes to your OpenAPI document, generate the SDK with the updated tags, and automatically increment the SDK's version number. +Speakeasy detects the OpenAPI changes, generates the SDK with updated tags, and +increments the SDK version number automatically. Take a look at the generated SDK to see how Speakeasy groups operations by tags. -In the SDK `README.md` file, you'll find documentation about your Speakeasy SDK. TypeScript SDKs generated with Speakeasy include an installable [Model Context Protocol (MCP) server](/docs/standalone-mcp/build-server) where the various SDK methods are exposed as tools that AI applications can invoke. Your SDK documentation includes instructions for installing the MCP server. - -Note that the SDK is not ready for production use. To get it production-ready, follow the steps outlined in your Speakeasy Studio workspace. +The SDK `README.md` file includes documentation for the Speakeasy SDK. +TypeScript SDKs generated with Speakeasy include an installable [Model Context +Protocol (MCP) server](/docs/standalone-mcp/build-server) where SDK methods are +exposed as tools for AI applications. The SDK documentation includes +instructions for installing the MCP server. -## How Speakeasy helps get your Pydantic models ready for SDK generation +The SDK is not production-ready until the steps in the Speakeasy Studio +workspace are completed. -In this tutorial, we learned how to generate an OpenAPI document from Pydantic models and use it to generate an SDK client using Speakeasy. +### Adding SDK generation to your CI/CD pipeline -If you would like to discuss how to get your Pydantic models ready for SDK generation, give us feedback, or shoot the breeze about all things OpenAPI and SDKs, [join our Slack](https://go.speakeasy.com/slack). +The Speakeasy +[`sdk-generation-action`](https://github.com/speakeasy-api/sdk-generation-action) +repository provides workflows for integrating the Speakeasy CLI into CI/CD +pipelines to automatically regenerate SDKs when Pydantic schemas change. -If you haven't already, take a look at our [blog](/blog) to learn more about API design, SDK generation, and our latest features, including: +Speakeasy can be set up to automatically push a new branch to SDK repositories +so that teammates can review and merge the SDK changes. -- [Native JSONL support in your SDKs](/blog/release-jsonl-support) -- [Introducing comprehensive SDK testing](/blog/release-sdk-testing) -- [Model Context Protocol: TypeScript SDKs for the agentic AI ecosystem](/blog/release-model-context-protocol) -- [Python generation with async and Pydantic support](/blog/release-python) -- [Choosing your Python REST API framework](/blog/choosing-your-framework-python) +For an overview of how to set up SDK automation, see the Speakeasy [SDK workflow +syntax reference](/docs/speakeasy-reference/workflow-file). diff --git a/public/assets/openapi/pydantic/scalar-post.png b/public/assets/openapi/pydantic/scalar-post.png new file mode 100644 index 00000000..49783fe0 Binary files /dev/null and b/public/assets/openapi/pydantic/scalar-post.png differ diff --git a/public/assets/openapi/pydantic/scalar.png b/public/assets/openapi/pydantic/scalar.png new file mode 100644 index 00000000..8b9362d0 Binary files /dev/null and b/public/assets/openapi/pydantic/scalar.png differ