Docementing Rest Api
Docementing Rest Api
with OpenAPI
In this chapter, you’ll learn to document APIs using OpenAPI: the most popular
standard for describing RESTful APIs, with a rich ecosystem of tools for testing, val-
idating, and visualizing APIs. Most programming languages have libraries that sup-
port OpenAPI specifications, and in chapter 6 you’ll learn to use OpenAPI-
compatible libraries from the Python ecosystem.
OpenAPI uses JSON Schema to describe an API’s structure and models, so we
start by providing an overview of how JSON Schema works. JSON Schema is a spec-
ification for defining the structure of a JSON document, including the types and
formats of the values within the document.
90
5.1 Using JSON Schema to model data 91
After learning about JSON Schema, we study how an OpenAPI document is struc-
tured, what its properties are, and how we use it to provide informative API specifica-
tions for our API consumers. API endpoints constitute the core of the specification, so
we pay particular attention to them. We break down the process of defining the end-
points and schemas for the payloads of the API’s requests and responses, step by step.
For the examples in this chapter, we work with the API of CoffeeMesh’s orders ser-
vice. As we mentioned in chapter 1, CoffeeMesh is a fictional on-demand coffee-delivery
platform, and the orders service is the component that allows customers to place and
manage their orders. The full specification for the orders API is available under
ch05/oas.yaml in the GitHub repository for this book.
A JSON Schema specification usually defines an object with certain attributes or prop-
erties. A JSON Schema object is represented by an associative array of key-value pairs.
A JSON Schema specification usually looks like this:
In this example, we define the schema of an object with one attribute named status,
whose type is string.
JSON Schema allows us to be very explicit with respect to the data types and formats
that both the server and the client should expect from a payload. This is fundamental
1
A. Wright, H. Andrews, B. Hutton, “JSON Schema: A Media Type for Describing JSON Documents” (Decem-
ber 8, 2020); https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-00. You can follow the devel-
opment of JSON Schema and contribute to its improvement by participating in its repository in GitHub:
https://github.com/json-schema-org/json-schema-spec. Also see the website for the project: https://json-schema
.org/.
92 CHAPTER 5 Documenting REST APIs with OpenAPI
for the integration between the API provider and the API consumer, since it lets us
know how to parse the payloads and how to cast them into the right data types in our
runtime.
JSON Schema supports the following basic data types:
string for character values
number for integer and decimal values
object for associative arrays (i.e., dictionaries in Python)
array for collections of other data types (i.e., lists in Python)
boolean for true or false values
null for uninitialized data
To define an object using JSON Schema, we declare its type as object, and we list its
properties and their types. The following shows how we define an object named order,
which is one of the core models of the orders API.
{
We can declare the
"order": {
schema as an object.
"type": "object",
"properties": {
We describe the object’s
"product": { properties under the
"type": "string" properties keyword.
},
"size": {
"type": "string"
},
"quantity": {
"type": "integer"
}
}
}
}
Since order is an object, the order attribute also has properties, defined under the
properties attribute. Each property has its own type. A JSON document that com-
plies with the specification in listing 5.1 is the following:
{
"order": {
"product": "coffee",
"size": "big",
"quantity": 1
}
}
As you can see, each of the properties described in the specification is used in this
document, and each of them has the expected type.
5.1 Using JSON Schema to model data 93
A property can also represent an array of items. In the following code, the order
object represents an array of objects. As you can see, we use the items keyword to
define the elements within the array.
{
"order": {
"type": "array", We define the elements
"items": { within the array using
"type": "object", the items keyword.
"properties": {
"product": {
"type": "string"
},
"size": {
"type": "string"
},
"quantity": {
"type": "integer"
}
}
}
}
}
In this case, the order property is an array. Array types require an additional property
in their schema, which is the items property that defines the type of each of the ele-
ments contained in the array. In this case, each of the elements in the array is an
object that represents an item in the order.
An object can have any number of nested objects. However, when too many objects
are nested, indentation grows large and makes the specification difficult to read. To
avoid this problem, JSON Schema allows us to define each object separately and to use
JSON pointers to reference them. A JSON pointer is a special syntax that allows us to
point to another object definition within the same specification.
As you can see in the following code, we can extract the definition of each item
within the order array as a model called OrderItemSchema and use a JSON pointer to
reference OrderItemSchema using the special $ref keyword.
{
"OrderItemSchema": {
"type": "object",
"properties": {
"product": {
"type": "string"
},
"size": {
"type": "string"
},
94 CHAPTER 5 Documenting REST APIs with OpenAPI
"quantity": {
"type": "integer"
}
}
},
"Order": {
"status": {
"type": "string"
},
"order": {
"type": "array",
We can specify the type
of the array’s items using
"items": {
a JSON pointer.
"$ref": '#/OrderItemSchema'
}
}
}
}
JSON pointers use the special keyword $ref and JSONPath syntax to point to another
definition within the schema. In JSONPath syntax, the root of the document is rep-
resented by the hashtag symbol (#), and the relationship of nested properties is
represented by forward slashes (/). For example, if we wanted to create a pointer
to the size property of the OrderItemSchema model, we would use the following syntax:
'#/OrderItemSchema/size'.
We can refactor our specification using JSON pointers by extracting common schema
objects into reusable models, and we can reference them using JSON pointers. This
helps us avoid duplication and keep the specification clean and succinct.
In addition to being able to specify the type of a property, JSON Schema also
allows us to specify the format of the property. We can develop our own custom for-
mats or use JSON Schema’s built-in formats. For example, for a property representing
a date, we can use the date format—a built-in format supported by JSON Schema that
represents an ISO date (e.g., 2025-05-21). Here’s an example:
{
"created": {
"type": "string",
"format": "date"
}
}
In this section, we’ve worked with examples in JSON format. However, JSON Schema
documents don’t need to be written in JSON. In fact, it’s more common to write them
5.2 Anatomy of an OpenAPI specification 95
in YAML format, as it’s more readable and easier to understand. OpenAPI specifica-
tions are also commonly served in YAML format, and for the remainder of this chap-
ter, we’ll use YAML to develop the specification of the orders API.
openapi: 3.0
info
servers
paths
components
2
For a detailed analysis of the differences between OpenAPI 3.0 and 3.1, check out OpenAPI’s migration from
3.0 to 3.1 guide: https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0.
3
According to the 2022 “State of the API” report by Postman (https://www.postman.com/state-of-api/api-
technologies/#api-technologies).
96 CHAPTER 5 Documenting REST APIs with OpenAPI
An OpenAPI specification contains everything that the consumer of the API needs to
know to be able to interact with the API. As you can see in figure 5.1, an OpenAPI is
structured around five sections:
openapi—Indicates the version of OpenAPI that we used to produce the
specification.
info—Contains general information, such as the title and version of the API.
servers—Contains a list of URLs where the API is available. You can list more
than one URL for different environments, such as the production and staging
environments.
paths—Describes the endpoints exposed by the API, including the expected pay-
loads, the allowed parameters, and the format of the responses. This is the most
important part of the specification, as it represents the API interface, and it’s the
section that consumers will be looking for to learn how to integrate with the API.
components—Defines reusable elements that are referenced across the specifi-
cation, such as schemas, parameters, security schemes, request bodies, and
responses.4 A schema is a definition of the expected attributes and types in your
request and response objects. OpenAPI schemas are defined using JSON Schema
syntax.
Now that we know how to structure an OpenAPI specification, let’s move on to docu-
menting the endpoints of the orders API.
4
See https://swagger.io/docs/specification/components/ for a full list of reusable elements that can be
defined in the components section of the API specification.
5.4 Documenting URL query parameters 97
The following shows the high-level definitions of the orders API endpoints. We
declare the URLs and the HTTP implemented by each URL, and we add an operation
ID to each endpoint so that we can reference them in other sections of the document.
paths:
/orders: We declare a URL path.
get:
operationId: getOrders An HTTP method supported
post: # creates a new order by the /orders URL path
operationId: createOrder
/orders/{order_id}:
get:
operationId: getOrder
put:
operationId: updateOrder
delete:
operationId: deleteOrder
/orders/{order_id}/pay:
post:
operationId: payOrder
/orders/{order_id}/cancel:
post:
operationId: cancelOrder
Now that we have the endpoints, we need to fill in the details. For the GET /orders
endpoint, we need to describe the parameters that the endpoint accepts, and for
the POST and PUT endpoints, we need to describe the request payloads. We also
need to describe the responses for each endpoint. In the following sections, we’ll
learn to build specifications for different elements of the API, starting with the
URL query parameters.
Both cancelled and limit can be combined within the same request to filter the results:
GET /orders?cancelled=true&limit=5
This request asks the server for a list of five orders that have been cancelled. Listing
5.5 shows the specification for the GET /orders endpoint’s query parameters. The
definition of a parameter requires a name, which is the value we use to refer to it in the
actual URL. We also specify what type of parameter it is. OpenAPI 3.1 distinguishes
four types of parameters: path parameters, query parameters, header parameters, and
cookie parameters. Header parameters are parameters that go in an HTTP header
field, while cookie parameters go into a cookie payload. Path parameters are part of
the URL path and are typically used to identify a resource. For example, in /orders/
{order_id}, order_id is a path parameter that identifies a specific order. Query
parameters are optional parameters that allow us to filter and sort the results of an
endpoint. We define the parameter’s type using the schema keyword (Boolean in the
case of cancelled, and a number in the case of limit), and, when relevant, we specify
the format of the parameter as well.5
Listing 5.5 Specification for the GET /orders endpoint’s query parameters
Now that we know how to describe URL query parameters, in the next section we’ll
tackle something more complex: documenting request payloads.
5
To learn more about the date types and formats available in OpenAPI 3.1 see http://spec.openapis.org/
oas/v3.1.0#data-types.
5.5 Documenting request payloads 99
{
"order": [
{
"product": "cappuccino",
"size": "big",
"quantity": 1
}
]
}
This payload contains an attribute order, which represents an array of items. Each
item is defined by the following three attributes and constraints:
product—The type of product the user is ordering.
size—The size of the product. It can be one of the three following choices:
small, medium, and big.
quantity—The amount of the product. It can be any integer number equal to
or greater than 1.
Listing 5.6 shows how we define the schema for this payload. We define request pay-
loads under the content property of the method’s requestBody property. We can
specify payloads in different formats. In this case, we allow data only in JSON format,
which has a media type definition of application/json. The schema for our payload
is an object with one property: order, whose type is array. The items in the array are
objects with three properties: the product property, with type string; the size prop-
erty, with type string; and the quantity property, with type integer. In addition, we
define an enumeration for the size property, which constrains the accepted values to
small, medium, and big. Finally, we also provide a default value of 1 for the quantity
property, since it’s the only nonrequired field in the payload. Whenever a user sends a
request containing an item without the quantity property, we assume that they want
to order only one unit of that item.
paths:
/orders:
post: We describe
operationId: createOrder request payloads
requestBody: under requestBody.
required: true
We specify
content:
whether the We specify the
payload is application/json:
payload’s content
required. schema:
type.
type: object
properties:
We define the order:
payload’s type: array
schema.
items:
type: object
properties:
100 CHAPTER 5 Documenting REST APIs with OpenAPI
product:
type: string
We can constrain the size:
property’s values using type: string
an enumeration. enum:
- small
- medium
- big
quantity:
type: integer
We specify a
required: false
default value.
default: 1
required:
- product
- size
Embedding payload schemas within the endpoints’ definitions, as in listing 5.6, can
make our specification more difficult to read and understand. In the next section, we
learn to refactor payload schemas for reusability and for readability.
Listing 5.7 Specification for the POST /orders endpoint using a JSON pointer
paths:
/orders:
post:
operationId: createOrder
requestBody:
required: true We use a JSON pointer to
content: reference a schema defined
application/json: somewhere else in the document.
schema:
$ref: '#/components/schemas/CreateOrderSchema'
components:
schemas:
Schema CreateOrderSchema:
definitions Every schema is an
type: object object, where the key is
go under properties:
components. the name and the values
order: are the properties that
type: array describe it.
items:
5.6 Refactoring schema definitions to avoid repetition 101
type: object
properties:
product:
type: string
size:
type: string
enum:
- small
- medium
- big
quantity:
type: integer
required: false
default: 1
required:
- product
- size
Moving the schema for the POST /orders request payload under the components sec-
tion of the API makes the document more readable. It allows us to keep the paths
section of the document clean and focused on the higher-level details of the endpoint.
We simply need to refer to the CreateOrderSchema schema using a JSON pointer:
#/components/schemas/CreateOrderSchema
The specification is looking good, but it can get better. CreateOrderSchema is a tad
long, and it contains several layers of nested definitions. If CreateOrderSchema grows
in complexity, over time it’ll become difficult to read and maintain. We can make it
more readable by refactoring the definition of the order item in the array in the fol-
lowing code. This strategy allows us to reuse the schema for the order’s item in other
parts of the API.
components:
schemas:
OrderItemSchema:
type: object
We introduce the
OrderItemSchema.
properties:
product:
type: string
size:
type: string
enum:
- small
- medium
- big
quantity:
type: integer
default: 1
CreateOrderSchema:
type: object
102 CHAPTER 5 Documenting REST APIs with OpenAPI
properties:
order:
type: array We use a JSON
items: pointer to point to
$ref: '#/OrderItemSchema' OrderItemSchema.
Our schemas are looking good! The CreateOrderSchema schema can be used to create
an order or to update it, so we can reuse it in the PUT /orders/{order_id} endpoint,
as you can see in listing 5.9. As we learned in chapter 4, the /orders/{order_id} URL
path represents a singleton resource, and therefore the URL contains a path parame-
ter, which is the order’s ID. In OpenAPI, path parameters are represented between
curly braces. We specify that the order_id parameter is a string with a UUID format (a
long, random string often used as an ID).6 We define the URL path parameter directly
under the URL path to make sure it applies to all HTTP methods.
paths:
/orders: We declare
get: the order’s
... resource URL. We define the URL
path parameter.
/orders/{order_id}: The order_id parameter
parameters: is part of the URL path.
- in: path
name: order_id
required: true The name of the parameter
We specify the schema:
parameter’s type: string The order_id
format (UUID). format: uuid parameter is required.
put:
operationId: updateOrder We define the HTTP
requestBody: method PUT for the
We document current URL path.
the request required: true
body of the content:
PUT endpoint. application/json:
schema:
$ref: '#/components/schemas/CreateOrderSchema'
Now that we understand how to define the schemas for our request payloads, let’s
turn our attention to the responses.
6
P. Leach, M. Mealling, and R. Salz, “A Universally Unique Identifier (UUID) URN Namespace,” RFC 4112
(https://datatracker.ietf.org/doc/html/rfc4122).
5.7 Documenting API responses 103
{
"id": "924721eb-a1a1-4f13-b384-37e89c0e0875",
"status": "progress",
"created": "2022-05-01",
"order": [
{
"product": "cappuccino",
"size": "small",
"quantity": 1
},
{
"product": "croissant",
"size": "medium",
"quantity": 2
}
]
}
This payload shows the products ordered by the user, when the order was placed, and
the status of the order. This payload is similar to the request payload we defined in sec-
tion 5.6 for the POST and PUT endpoints, so we can reuse our previous schemas.
components:
schemas:
OrderItemSchema:
... We define the
GetOrderSchema
GetOrderSchema: schema.
type: object
properties:
We constrain the values of
status:
the status property with
type: string
an enumeration.
enum:
- created
- paid
- progress
- cancelled
- dispatched
- delivered
created:
A string with
type: string
date-time format
format: date-time
order:
We reference the
OrderItemSchema
type: array
schema using a
items:
JSON pointer.
$ref: '#/components/schemas/OrderItemSchema'
allOf is used in these cases to indicate that the object requires all the properties in
the listed schemas.
components:
schemas:
OrderItemSchema:
... We use the allOf keyword
to inherit properties from
GetOrderSchema: We use a JSON
other schemas.
allOf: pointer to reference
- $ref: '#/components/schemas/CreateOrderSchema' another schema.
- type: object
properties: We define a new object to
status: include properties that are
type: string specific to GetOrderSchema.
enum:
- created
- paid
- progress
- cancelled
- dispatched
- delivered
created:
type: string
format: date-time
Model composition results in a cleaner and more succinct specification, but it only
works if the schemas are strictly compatible. If we decide to extend CreateOrderSchema
with new properties, then this schema may no longer be transferable to the GetOrder-
Schema model. In that sense, it’s sometimes better to look for common elements among
different schemas and refactor their definitions into standalone schemas.
Now that we have the schema for the GET /orders/{order_id} endpoint’s response
payload, we can complete the endpoint’s specification. We define the endpoint’s
responses as objects in which the key is the response’s status code, such as 200. We also
describe the response’s content type and its schema, GetOrderSchema.
paths:
/orders:
5.8 Creating generic responses 105
get:
...
/orders/{order_id}:
parameters:
- in: path
name: order_id
required: true
schema:
type: string
format: uuid
As you can see, we define response schemas within the responses section of the end-
point. In this case, we only provide the specification for the 200 (OK) successful
response, but we can also document other status codes, such as error responses. The next
section explains how we create generic responses we can reuse across our endpoints.
We name the
response. Generic responses go
components:
under responses in the
components section.
responses:
NotFound: We describe
description: The specified resource was not found. the response.
106 CHAPTER 5 Documenting REST APIs with OpenAPI
content:
We define
application/json:
the response’s
content. schema:
$ref: '#/components/schemas/Error'
We reference the
Error schema.
schemas:
OrderItemSchema:
...
Error:
We define the
type: object
schema for the
properties: Error payload.
detail:
type: string
required:
- detail
This specification for the 404 response can be reused in the specification of all our
endpoints under the /orders/{order_id} URL path, since all of those endpoints are
specifically designed to target a specific resource.
NOTE You may be wondering, if certain responses are common to all the end-
points of a URL path, why can’t we define the responses directly under the
URL path and avoid repetition? The answer is this isn’t possible as of now. The
responses keyword is not allowed directly under a URL path, so we must docu-
ment all the responses for every endpoint individually. There’s a request in the
OpenAPI GitHub repository to allow including common responses directly
under the URL path, but it hasn’t been implemented (http://mng.bz/097p).
We can use the generic 404 response from listing 5.13 under the GET /orders/
{order_id} endpoint.
Listing 5.14 Using the 404 response schema under GET /orders/{order_id}
paths:
...
/orders/{order_id}:
parameters:
- in: path
name: order_id
required: true
schema:
type: string
"format": uuid
get:
summary: Returns the details of a specific order
operationId: getOrder
responses:
'200':
description: OK
content:
application/json:
5.9 Defining the authentication scheme of the API 107
schema:
We define $ref: '#/components/schemas/GetOrderSchema' We reference the
a 404 '404': NotFound response
response. $ref: '#/components/responses/NotFound' using a JSON pointer.
The orders API specification in the GitHub repository for this book also contains a
generic definition for 422 responses and an expanded definition of the Error compo-
nent that accounts for the different error payloads we get from FastAPI.
We’re nearly done. The only remaining endpoint is GET /orders, which returns a
list of orders. The endpoint’s payload reuses GetOrderSchema to define the items in
the orders array.
paths:
/orders: We define the new
get: GET method of the
operationId: getOrders /orders URL path.
responses:
'200':
description: A JSON array of orders
content:
application/json:
schema:
type: object
properties:
orders is
orders:
an array.
type: array
items:
$ref: '#/components/schemas/GetOrderSchema'
required:
- order Each item in the array is
defined by GetOrderSchema.
post:
...
/orders/{order_id}:
parameters:
...
Our API’s endpoints are now fully documented! You can use many more elements
within the definitions of your endpoints, such as tags and externalDocs. These attri-
butes are not strictly necessary, but they can help to provide more structure to your
API or make it easier to group the endpoints. For example, you can use tags to create
groups of endpoints that logically belong together or share common features.
Before we finish this chapter, there’s one more topic we need to address: docu-
menting the authentication scheme of our API. That’s the topic of the next section!
security schemes. The security definitions of the API go within the components section
of the specification, under the securitySchemes header.
With OpenAPI, we can describe different security schemes, such as HTTP-based
authentication, key-based authentication, Open Authorization 2 (OAuth2), and OpenID
Connect.7 In chapter 11, we’ll implement authentication and authorization using the
OpenID Connect and OAuth2 protocols, so let’s go ahead and add definitions for
these schemes. Listing 5.16 shows the changes we need to make to our API specifica-
tion to document the security schemes.
We describe three security schemes: one for OpenID Connect, another one for
OAuth2, and another for bearer authorization. We’ll use OpenID Connect to authorize
user access through a frontend application, and for direct API integrations, we’ll offer
OAuth’s client credentials flow. We’ll explain how each protocol and each authorization
flow works in detail in chapter 11. For OpenID Connect, we must provide a configura-
tion URL that describes how our backend authentication works under the
openIdConnectUrl property. For OAuth2, we must describe the authorization flows
available, together with a URL that clients must use to obtain their authorization tokens
and the available scopes. The bearer authorization tells users that they must include a
JSON Web Token (JWT) in the Authorization header to authorize their requests.
components:
responses:
...
The security schemes under We provide a name for
the securitySchemes header the security scheme (it
schemas:
of the API’s components can be any name).
... section
securitySchemes: The type of
openId: security scheme
The name type: openIdConnect
of another openIdConnectUrl: https://coffeemesh-dev.eu.auth0.com/.well-
security ➥ known/openid-configuration The URL that describes the
scheme oauth2:
A description OpenID Connect configuration
type: oauth2 in our backend
The type of of the client
flows:
the security credentials flow
scheme clientCredentials:
tokenUrl: https://coffeemesh-dev.eu.auth0.com/oauth/token
The authorization scopes: {}
bearerAuth: The available scopes The URL where
flows available
under this type: http when requesting an users can request
security scheme scheme: bearer authorization token authorization tokens
bearerFormat: JWT
... The bearer token has
a JSON Web Token
(JWT) format.
7
For a complete reference of all the security schemas available in OpenAPI, see https://swagger.io/docs/
specification/authentication/.
Summary 109
security:
- oauth2:
- getOrders
- createOrder
- getOrder
- updateOrder
- deleteOrder
- payOrder
- cancelOrder
- bearerAuth:
- getOrders
- createOrder
- getOrder
- updateOrder
- deleteOrder
- payOrder
- cancelOrder
This concludes our journey through documenting REST APIs with OpenAPI. And
what a ride! You’ve learned how to use JSON Schema; how OpenAPI works; how to
structure an API specification; how to break down the process of documenting your
API into small, progressive steps; and how to produce a full API specification. The
next time you work on an API, you’ll be well positioned to document its design using
these standard technologies.
Summary
JSON Schema is a specification for defining the types and formats of the prop-
erties of a JSON document. JSON Schema is useful for defining data validation
models in a language-agnostic manner.
OpenAPI is a standard documentation format for describing REST APIs and
uses JSON Schema to describe the properties of the API. By using OpenAPI,
you’re able to leverage the whole ecosystem of tools and frameworks built around
the standard, which makes it easier to build API integrations.
A JSON pointer allows you to reference a schema using the $ref keyword. Using
JSON pointers, we can create reusable schema definitions that can be used in dif-
ferent parts of an API specification, keeping the API specification clean and easy
to understand.
An OpenAPI specification contains the following sections:
– openapi—Specifies the version of OpenAPI used to document the API
– info—Contains information about the API, such as its title and version
– servers—Documents the URLs under which the API is available
– paths—Describes the endpoints exposed by the API, including the schemas
for the API requests and responses and any relevant URL path or query
parameters
– components—Describes reusable components of the API, such as payload
schemas, generic responses, and authentication schemes