Education · Jan 15, 2024
How to write JSON Schemas for your API Part 2: documenting fields
JSON Schema serves two purposes when used for APIs. The first is to validate incoming data. The second is to communicate to clients how they should format their data so it passes validation.
JSON Schema supports several meta-data keywords that don't participate in validation logic but help the developer to understand the schema.
Many documentation tools can take this meta-data and produce publishable documentation for developers.
Human-readable documentation
title and description
Each schema can be given a title and description, which can be displayed by documentation or user interfaces. The title is typically a single line, whereas the description can contain paragraphs.
{
"title": "Contact",
"description": "The contact object describes the contact information for a person associated with an account.\n\nAn account can have many contacts, and a single contact can belong to more than one account.",
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}
Since most descriptions contain multiple lines of text, it can be helpful to write JSON schema as YAML instead of JSON because it supports line breaks
title: Contact
description: |
The contact object describes the contact information for a person associated with an account.
An account can have many contacts, and a single contact can belong to more than one account.
type: object
properties:
name:
type: string
examples
One of the most helpful ways to onboard new developers onto your API is to provide them with realistic examples, which they can copy and modify.
Using examples
you can include illustrate or more use-cases using concrete examples. Most documentation websites will read this and display examples side-by-side with field-level documentation.
Here is a schema for addresse that includes examples for different regions.
title: Address
type: object
properties:
line_1:
type: string
line_1:
type: string
city:
type: string
state:
type: string
zip:
type: string
country:
type: string
examples:
united_states:
line_1: 10 Example St
line_2: Apt 3
city: Chigaco
state: IL
zip: 60007
country: USA
australia:
line_1: 3/10 Example St
city: Moorabbin
state: VIC
zip: 3189
country: AUSTRALIA
API behavior
default
In the case of optional data, the schema can be annotated with the value that the API will use by default.
This can make your API more ergonomic and simpler to use, similar to progressive disclosure.
Using the printer example, a print API can specify a default printer setting preset.
type: object
properties:
printer_settings:
type: string
default: black_and_white
Or, if the user specifies a custom preset, they only need to specify the settings they care about.
type: object
properties:
paper_size:
type: string
default: A4
color:
type: boolean
default: false
double_sided:
type: boolean
default: true
Specifying a default also clarifies to the developer that the omission of an optional field is not an additional state, but rather equivalent to one of the existing states. For example, an optional boolean field without a default value could be interpreted by some APIs as either true, false or undefined as a third state.
readOnly
When it comes to APIs, it is useful to share schemas between the requests and responses. However in practice there are some fields that should only appear in requests or only appear in responses. A good example is ID fields, which need to be generated by the server to ensure uniqueness. Create APIs usually should not require IDs.
You can use readOnly
to specify that the property only appears in responses.
type: object
properties:
id:
type: string
format: uuid
description: A unique identifier for the object, generated by the server upon creation.
readOnly: true
The advantage of specifying a field as read only is that you can reuse the same schema in both request and response contents. This allows developers to model the request and response using the same type in code.
writeOnly
The writeOnly
keyword is the opposite to the readOnly
keyword, as you would expect. Though there are fewer use cases.
An example of a field that should only appear in requests is a password field of a Reset Password flow. The server should be able to accept new passwords, but they should never appear in any response for security reasons.
type: object
properties:
new_password:
type: string
writeOnly: true
API product lifecycle
deprecated
APIs products have their own lifecycle just like other digital products. However, unlike web and mobile products, they have an additional constraint to ensure backwards compatibility and not break existing integrations.
A common way to strike this balance is to deprecate certain fields in your API. When you deprecate a field, it is still functional but communicates to developers that they should stop relying on it soon. (Your description should also include details on how to migrate away from using the field.)
This example shows an is_active
boolean field that has been deprecated in favor of a more flexible status
field.
type: object
title: User
properties:
is_active:
type: boolean
deprecated: true
description: |
Whether the user is active.
This field is deprecated and will be removed in a future version. Use the `status` instead.
status:
type: string
enum:
- active
- inactive
- suspended