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