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
- Clash with JSON Merge Patch / RFC 7396. They cannot be expressed because the standard reserves
null
for deletions; and - 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.
null¶
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¶
Datetimes¶
Use RFC 3339, including a UTC offset. Note that the UTC offset is written with a hyphen (technically a hyphen-minus), not a minus sign. Use only IANA timezones. Note that OpenAPI uses this format for date-time
and date
types. For example:
{
"date-time": "2023-11-02T14:55:00-08:00",
"timezone": "America/Los_Angeles"
}
In some cases, it may be acceptable to append the timezone like this: {date-time} [{timezone}]
; e.g. 2023-11-02T14:55:00-08:00 [America/Los_Angeles]
. This format is generally preferred in documentation.
Durations and intervals¶
A duration may be written these three ways:
- A number of days, hours, minutes, seconds, etc.;
- An ISO 8601 duration using only hours, minutes, and seconds and starting with
PT
; or - Hours, minutes and seconds (
HH:MM:SS[.iii[iii]]
).
regex
For ISO 8601:
^\
PT\
(?:(\d)H)??\
(?:(\d++)M)??\
(?:(\d++(?:\.\d{1,6}++)?+)S)?+\
$
For HH:MM:SS:
```regex
^\
(\d{2,}+)\
:([0-5]\d)\
:([0-5]\d)\
(?:\.(\d{3}|\d{6}))?+\
$
Rationale
ISO 8601 is a convoluted mess of excessive complexity, confusing syntax, and ambiguity. Unfortunately, its duration format is already in use.
Year and month are ambiguous. Months have variable numbers of days, and year could be 365 days, mean solar year (365.24217 solar days), or a specific solar year. Days could be defined as exactly 24 hours, but PT<h>H[<m>M[<s>S]]
is easier to parse and convert to hh:mm:ss
.
Examples
✅ ok 35.2
for a key duration_sec
✅ ok PT23H45M55.8S
(per the spec, 8S
means 8 milliseconds)
✅ ok 23:45:55.800
(800 milliseconds)
✅ ok 23:45:55.800200
(800 milliseconds and 200 microseconds)
❌ not ok 23:45:55.2
– ambiguous: is .8
8 milliseconds or 800
?
❌ not ok P6M2WT45M55S
– ambiguous because months have indeterminate durations
❌ not ok P1D12H
– unambiguous but not limited to hours, minutes, and seconds
❌ not ok P2S
– does not start with PT
; rewrite as PT2S
❌ 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.
OpenAPI
In OpenAPI, you can use these schema object definitions:
timepoint:
oneOf:
- $ref: '#/components/schemas/iso-duration'
- $ref: '#/components/schemas/hhmmss-duration'
iso-duration:
type: string
pattern: >-
^\
PT\
(?:(\d)H)??\
(?:(\d++)M)??\
(?:(\d++(?:\.\d{1,6}++)?+)S)?+\
$
hhmmss-duration:
type: string
pattern: >-
^\
(\d{2,}+)\
:([0-5]\d)\
:([0-5]\d)\
(?:\.(\d{3}|\d{6}))?+\
$
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 may only use response codes in accordance with this guideline. The acceptable responses and conditions are listed in two tables below. These are exhaustive; servers must not use status codes, methods, responses, or conditions not listed here.
General status codes:
Code | Name | Methods | Response | Condition(s) |
---|---|---|---|---|
200 | OK | HEAD, GET, PATCH | resource | The requested resource is being returned. |
201 | Created | POST, PUT | canonical URI | The resource has been created. |
202 | Accepted | POST, PUT, PATCH¹, DELETE | ∅ | The request will be processed asynchronously. |
204 | No Content | DELETE | ∅ | The deletion was successful. |
308 | Permanent Redirect | any | resource | A non-canonical URI was used, and a permanent redirect is provided to the canonical URI. |
400 | Bad Request | any | problem details² | The endpoint does not exist, the parameters are wrong, or the body is malformed. |
401 | Unauthorized | any | problem details | Authentication was required but not provided. |
403 | Forbidden | any | problem details | The provided authentication carries insufficient privileges. |
404 | Not Found | GET, PATCH, DELETE | problem details | The requested resource does not exist. |
406 | Not Acceptable | HEAD, GET | problem details | The Accept headers are unsatisfiable. |
409 | Conflict | POST, PUT, PATCH | problem details | The resource already exists. |
409 | Conflict | DELETE | problem details | The resource cannot be deleted because other resources reference it. |
410 | Gone | GET, PATCH, DELETE | problem details | The resource does not exist, although it did before. |
413 | Content Too Large | POST, PUT, PATCH | problem details | The request payload is too large. |
415 | Unsupported Media Type | POST, PUT, PATCH | problem details | The request payload’s media type is unsupported. |
422 | Unprocessable Entity | POST, PUT, PATCH | problem details | The request was readable but contained semantic errors, such as invalid references. |
429 | Too Many Requests | any | problem details | The client has exceeded the rate limit. |
500 | Server Error | any | problem details | The server encountered an internal error. |
503 | Service Unavailable | any | problem details | The service is overloaded or down for maintenance. |
- Use JSON Merge Patch for all PATCH requests; see the JSON Merge Patch section.
- Use RFC 9457 problem details; see the problem details section.
Specialized status codes:
Code | Name | Methods | Response | Use case |
---|---|---|---|---|
100 | Continue¹ | POST, PUT, PATCH | ∅ | The 100-continue request has succeeded (rare). |
206 | Partial Content | GET | part | A range was requested and is being returned. |
304 | Not Modified | HEAD, GET | ∅ | The If-None-Match condition has matched. |
412 | Precondition Failed¹ | POST, PUT, PATCH, DELETE | problem details | A mid-air edit condition (using If-... headers) failed. |
416 | Range Not Satisfiable | GET | problem details | The requested range is out of bounds. |
417 | Expectation Failed¹ | POST, PUT, PATCH | problem details | The Expect: 100-continue expectation failed. |
418 | I’m a Teapot | any | problem details | The request is blocked due to suspicious or malicious activity, or excessive data. |
423 | Locked | POST, PUT, PATCH, DELETE | problem details | (strongly discouraged) A needed resource is “in use”. |
428 | Precondition Required¹ | POST, PUT, PATCH, DELETE | problem details | A precondition (using If-... headers) is required. |
431 | Request Header Fields Too Large | any | problem details | The headers are too large. |
† These statuses are only applicable to modifiable resources.
404 Not Found¶
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 details) should describe the problem; e.g. {..., "detail": "{id} must match ^[0-9A-F]{16}$"}
.
Occasionally, the server might know that the resource will exist later. For example, if the client PUT a resource, received a 202 Accepted, and then tried to GET the resource too early. A 404 is still appropriate for this. You can indicate the resource’s status in the problem details (detail
and/or custom field).
422 Unprocessable Entity and 409 Conflict¶
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¶
This is an optional response. 418 I’m a Teapot may be used to communicate with a client that has been locked out, for reasons other than ratelimiting. Although this status is nonstandard, some servers use it for similar reasons.
Clients should respond to these situtations very differently than to other 4xx responses, such as 401, 403, and 429. A 418 conveys that:
- The client cannot rectify the problem;
- The server may or may not be willing to process a different request; and
- The server may or may not accept the same request if it is re-sent later.
Some use cases:
- Suspicious queries: The client has previously sent several suspicious queries.
- Clearly malicious query: The client is currently sending a query that is clearly malicious.
- Excessive data: The client has sent an excessive amount of data over a short period.
JSON Merge Patch¶
For JSON, all PATCH endpoints must use JSON Merge Patch. Because JSON Merge Patch uses null
to signal deletion, null
may not be used for any other purpose. See the null section for more information.
For non-JSON data, there are two options:
- Use JSON Merge Patch with a JSON string. If the data is binary, encode it as base64.
- Use a multipart request with a JSON Merge Patch, with the Merge Patch first. The Merge Patch might choose to reference the additional files by filename.
Problem details¶
Use RFC 9457 (“problem details”). All 4xx and 5xx responses must include an RFC 9457 body with media-type application/problem+json
.
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"
}
Keys:
title
(required): A short, human-readable title that ends with a period.detail
(required sans 500 Server Error and 418 I’m a Teapot): A human-readable description of the problem in one or more complete sentences.type
(optional; if used, it should be used for all responses): A URI at which the client can find more information about the problem type (see below).status
(optional): The status code (e.g.400
) shared with the response status code.instance
(optional; if used, it should be used for all responses sans 500 and 418): A URI that identifies the specific occurrence of the problem for this response.- extensions: Any additional keys that a client may find useful.
There must be a 1-1 correspondence between type
and title
. It is recommended that the same type
/title
only map to one status code.
type
¶
Example: https://domain.tld/help/error/client#dsl-parse
As shown in the example, a URI fragment is perfectly acceptable. The response body must include the problem detail’s title
alongside a more detailed description.
Multiple representations must be available via content negotiation:
text/html; charset=utf-8
(required per RFC 9457): Include the title in an<h1>
, ⋯,<h6>
.application/json
(required): Include at least the keystitle
anddescription
.text/x-markdown
(recommended): Include the title in an#
, ⋯,#####
. If OpenAPI is used, use the schema’sresponse.description
.
Extensions¶
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. These extensions might occasionally be redundant to headers, such as Accept
and RateLimit-Limit
; that’s fine.
Response headers¶
Links¶
If HATEOAS links 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
. To avoid polluting JSON response bodies, put the links in Link
headers or See
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 along with a Retry-After
header. RateLimit-Limit
, RateLimit-Remaining
, and RateLimit-Reset
MAY be included for other responses as well.
Content-Disposition¶
Include Content-Disposition: attachment
where it would be useful for browsers, even if access via browsers is not expected. This should include responses of > 10 MB. The filename should include the resource type, a resource id (or other value with a 1-1 correspondence with the resource), and a filename extension. Example: Content-Disposition: attachment; filename="store-item-5221-3q.parquet"
Warnings¶
Use the nonstandard header Warning
for non-fatal issues. The header should follow this format:
Warning: <description>{; <key>="<value>"}
Examples
- “Warning: deprecated endpoint; use-instead=”https://domain.tld/api/v2/endpoint“
- “Warning: non-canonical URI; canonical-uri=”https://domain.tld/api/v2/search?filter[1]=color:eq:red|name:eq:apple“
Other headers¶
Include:
Content-Length
Content-Range
for 206 Partial Response responsesLocation
for 201 Created responsesETag
for modifiable resourcesLast-Modified
for modifiable resources (optionally)Vary
(optionally)Cache-Control
(optionally)
You can omit Date
, Age
, Origin
, Host
, Server
, and From
. If they’re already being sent, that’s also fine.