Skip to content

API conventions

JSON and data representation

Use JSON Schema, version 2020-12.

Follow the Google JSON guide. Contradicting that guide, property names may follow other conventions if needed to accommodate other needs. Avoid periods (.) in property names; doing so complicates using JsonPath, JMESPath, jq, YAML, and TOML. Permit only 1 type for a given key or array.

Null and missing values; numerical range and precision

Rationale

Null values

  1. Clash with JSON Merge Patch / RFC 7396. They cannot be expressed because the standard reserves null for deletions; and
  2. Have no agreed-upon or obvious meaning. They could signal an invalid value, a truly missing value (i.e. never found/added), or a selective decision to exclude (e.g. to save bandwidth).

About using null for missing-but-schema-supported keys: This practice is not obvious, wastes bandwidth, is supurfulous to the schema, and breaks if the schema changes. Let the schema specify what keys are allowed.

Do not use JSON null, except in JSON Merge Patch. JSON Merge Patch uses it to signal deletion, so using null for other purposes effectively prevents HTTP PATCH. It’s problematic for other reasons; refer to the rationale box. Tip: replace any {"key": null} with {} or (e.g.) {"status: "error:too_few_samples}"}. If a null value is encountered (e.g. in a received payload), pretend it isn’t there.

NaN, Inf, and -Inf

JSON does not support numerical NaN, Inf, or -Inf. If NaN or infinite values are applicable for a given key or array, encode them as strings. For example, if values in an array can be a float, Inf, or -Inf, encode them like this: ["5.3", "Inf", "6.8", "-Inf"]. Write literally NaN, Inf, or -Inf, with that capitalization. (The minus sign must be U+002D.)

Range and precision

Generally, assume that JSON consumers will use IEEE 754 double range and precision. When writing numbers that might exceed that range or precision (where that precision is important), encode them as strings (as with NaN, Inf, and -Inf).

Case study: Representing inconclusive or unknown values

Null values are often used to encode types like string | null and int | null. Instead, be explicit about null’s meaning.

Consider a sensor measuring electric current. The values are then divided by preceeding average to get a ratio, that is, \(R(I_t) = \left. I_t \middle/ \text{Avg}_{i=1}^{t-1} I_i \right.\) , where \(I_t\) is the current for trial \(t\). Compare these two representations:

Did the measurement fail? A hardware connection issue? Was the ratio Inf (1/0), -Inf (-1/0), or NaN (0/0)?

[
  {"trial": 1, "value": 12.0},
  {"trial": 2, "value": null},
]

Specify the status values with a JSON Schema enum. The simpler alternative {"success": <boolean>} could work, too.

[
  {"trial": 1, "status": "success", "value": 12.0},
  {"trial": 2, "status": "error:no_signal"}
]

Encoding specific types

Dates and times

Use RFC 3339, including a UTC offset. Note that the UTC offset is written with a hyphen, not a minus sign. Use only IANA timezones. For example:

{
  "date-time": "2023-11-02T14:55:00 -08:00",
  "timezone": "America/Los_Angeles"
}

Durations and intervals

A duration may be written as (1) a number of days, hours, minutes, seconds, etc.; (2) an ISO 8601 duration starting with PT; or (3) HH:MM:SS[.iii[iii]].

Examples

✅ ok 35.2 for a key duration_sec

✅ ok PT23H55M55S

✅ ok 23:45:55

❌ Not ok P6M2WT45M55S (ambiguous – months have indeterminate durations)

❌ Not ok P2S (unambiguous but does not start with PT)

❌ Not ok 05:22 (is this min:sec or hour:min?)

For intervals, both {"start": ..., "end": ...} and ISO 8601 T1--T2 syntax are acceptable. Do not separate times with / or use a start-time/duration pair.

Warning

Be careful when calculating durations. Things like NTP synchronization events can cause \(T^C_1 - T^C_2\) for a clock \(C\) to not correspond to an elapsed time (or true duration).

HTTP APIs

Status codes

This section applies to REST-like HTTP APIs. Servers should only issue response codes in accordance with the following table.

Note: Few services need every response in this table.

code name methods response use case
100 Continue¹ P/P/P 100-continue succeeded
200 OK HEAD/GET/PATCH resource
201 Created POST/PUT uri‡
202 Accepted P/P/P/D§
204 No Content DELETE Successful deletion
206 Partial Content GET part Range was requested
304 Not Modified HEAD/GET If-None-Match matches
308 Permanent Redirect any resource Point to canonical URI
400 Bad Request² any error♯ Invalid endpoint, body, etc.
401 Unauthorized any error Not authenticated
403 Forbidden any error Insufficient privileges
404 Not Found² GET/DELETE/PATCH error No such resource (e.g. by id)
406 Not Acceptable³ HEAD/GET error Accept headers unsatisfiable
409 Conflict⁴ P/P/P error Resource already exists
409 Conflict⁴ DELETE error Other resources depend on this
410 Gone³ GET/DELETE/PATCH error Resource removed
412 Precondition Failed¹ P/P/P/D error Mid-air edit (If-...)
413 Content Too Large³ P/P/P error
414 URI Too Long³ GET error
415 Unsupported Media Type P/P/P error Invalid payload media type
416 Range Not Satisfiable GET error Requested range out of bounds
417 Expectation Failed¹ P/P/P error Expect: 100-continue failed
422 Unprocessable Entity⁴ P/P/P error Sematic; invalid reference, etc.
418 I’m a teapot⁵ any error Request blocked
428 Precondition Required¹ P/P/P/D error If-... required
431 … Fields Too Large² any error
429 Too Many Requests any error Ratelimit hit
500 Server Error any error General server error
503 Service Unavailable any error Maintenance or overload

Footnotes: - † POST/POST/PATCH - ‡ A JSON document containing (at least) uri; e.g. {"uri": "https://domain.tld/api/thing/1"}. The value should be the canonical URI for which GET returns the resource. - § POST/POST/PATCH/DELETE - ♯ An RFC 7807 JSON payload, as described below

PATCH should use JSON Merge Patch (assuming JSON).

Notes about specific codes: - ¹ Useful for certain modifiable resources. - ² 404 Not Found is reserved for resources that could exist but do not; attempts to access an invalid endpoint must always generate a 400 (Bad Request). For example, if id must be hexadecimal for /machine/{id}, then /machine/zzz should generate a 400. The response body (RFC 9457 problem detials) can (and likely should) describe the prbolem; e.g. {..., "detail": "{id} must match ^[0-9A-F]{16}$"}. - ³ Preferred but optional. A 400 Bad Request may be used instead. - ⁴ Use 422 Unprocessable Entity for errors with respect to the model semantics and/or data. For example, in {"robot: "22-1-44", "action": "sit"}, a 422 might be sent if robot 22-1-44 does not exist or lacks sit/stand functionality. A 409 Conflict might result if it 22-1-44 cannot accept the command because it is currently handling another. Respond 409 Conflict for conflicting state, most notably to a request to delete a resource that other resources reference. - ⁵ 418 I’m a Teapot may optionally be used to communicate with a client that has been locked out for reasons other than ratelimiting. For example, this might be sent if the client previously sent several suspicious queries, is sending a query that appears malicious, or sent an excessive amount of data over the last few minutes.
While nonstandard, 418 is sometimes used this way to distinguish these types of situtations from more common ones. A 418 indicates that the client cannot rectify the problem, that the server may or may not be willing to process a different request, and that the server may or may not accept the same request if it is re-sent later. These factors cleanly distinguish 418 from 403 Forbidden, 429 Too Many Requests, etc. Note that simply refusing connections is an alternative but may be more frustrating to users.

4xx/5xx error responses

All 4xx and 5xx responses should include a RFC 9457 body with media-type application/problem+json. title is required. It should be a short, free-form, human-readable statement that ends with a period. detail should similarly contain free-form, human readable statements (one or more sentences). type must be in either all or no responses; same for status.

type must serve application/json and text/html; charset=utf-8 (which RFC 9457 requires). text/x-markdown is also recommended, preferably identical to the corresponding OpenAPI schema response.description.

Always include extensions that a client would likely want to parse. For example, specify the incorrect request parameter or header, or the dependent resource for a 409 Conflict. Use kebab-case or camelCase according to the convention your overall API uses. Occasionally, these extensions will be redundant to headers, such as Accept and RateLimit-Limit; this is ok.

Examples
{
  "title": "Internal server error",
  "type": "https://domain.tld/help/error/server.internal"
  "status": "500",
}
{
  "title": "DSL parse error",
  "type": "https://domain.tld/help/error/client.dsl-parse"
  "status": "422",
  "detail": "Line number 22 contains an unidentified symbol '@' at column 14.",
  "lineNumber": 22,
}
{
  "title": "Malformed parameter.",
  "status": "400",
  "detail": "Parameter 'name' is malformed. It must be a 10-digit hexadecimal string.",
  "parameter": "name"
}

If links per HATEOAS are used, they should be limited to direct connections. For example, if a species resource links to its genus, which links to family, species should not link to family.

Headers

Content types

Provide Accept: on non-HEAD requests – for example, Accept: text/json. Similarly, provide Content-Type: on POST – for example, Content-Type: text/json.

Rate-limiting

Use draft IETF rate-limiting headers: RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset. These should always be included for 429 (Too Many Requests) responses and MAY be included for other responses as well.

Location

Always include Location for 201 Created responses.

Formal grammars

Rationale

=/ modifies an already-defined rule, which complicates reading. LWSP is commonly understood to be problematic.

Grammars may be specified in any well-defined form. ABNF (see RFC5234), XML’s custom meta-grammar, and regex-BNF are recommended.

With ABNF, do not use the incremental alternatives notation (=/), and avoid the core rules CHAR, LWSP, CTL, VCHAR, and WSP.