Payload Conventions
Rules for JSON structure, media types, and consistent payload conventions.
These rules standardize JSON payload shapes so clients can parse consistently and services can evolve safely.
Related guides:
Related reference:
JSON Structure & Properties
#704 - Use Standard Non-JSON Media Types
Intent: support downloads/exports without forcing JSON.
When non-JSON content types are needed (e.g., binary files, images, CSV), use standard content negotiation with appropriate Content-Type headers and follow HTTP conventions.
Patterns:
- File download endpoint:
GET /reports/123/content
Accept: application/pdfHTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="report-123.pdf"- Binary upload endpoint:
PUT /avatars/123
Content-Type: image/pngLarge exports (recommended):
- Prefer an async job resource:
POST /exports→202 Accepted+ status URLGET /exports/{id}→ status + link to download
Rules:
- Always set the correct
Content-Type. - Use
Content-Dispositionfor downloads when appropriate. - Keep JSON for error responses even when the success body is non-JSON (see [#404]).
#705 - Use Standard Media Types
Intent: maximize compatibility across tools and clients.
APIs SHOULD use standard IANA-registered media types.
Defaults:
- Success JSON:
application/json - Error responses:
application/problem+json(see [#404])
Common non-JSON cases:
- File download:
application/pdf,text/csv,application/zip, etc. - Binary upload/download:
application/octet-stream - Multipart upload:
multipart/form-data
Rules:
- Always set the correct
Content-Typefor responses. - Require clients to send the correct
Content-Typefor requests with bodies. - Avoid custom/vendor media types unless you have a strong compatibility reason.
Testability:
- OpenAPI declares correct content types for request/response bodies.
#706 - Define Maps with additionalProperties
Intent: keep schemas valid and codegen-friendly when keys are dynamic.
When representing a map/dictionary with dynamic keys in JSON, model it as an object with additionalProperties describing the value type.
Requirements:
- Document the key space (what keys mean, how they are constructed, constraints).
- Document whether keys are stable (contracted) or arbitrary (user-provided).
- Avoid maps when the key set is actually bounded/known: use explicit properties or an array of objects instead.
OpenAPI examples:
- Map of string → string:
type: object
description: Map from feature flag name to state.
additionalProperties:
type: string- Map of string → object:
type: object
description: Map from attribute name to attribute metadata.
additionalProperties:
$ref: "#/components/schemas/AttributeMetadata"Alternative (prefer when ordering/duplication matters):
If clients need ordering, duplicates, or additional per-entry metadata, prefer an array:
{
"attributes": [
{ "key": "department", "value": "Engineering" },
{ "key": "region", "value": "US" }
]
}Notes:
- Free-form maps make filtering, sorting, and partial updates harder; use them intentionally.
- If keys are user-provided, call out any normalization rules (case sensitivity, allowed characters).
#708 - Avoid Nested Objects
Intent: reduce payload complexity and improve evolvability.
APIs SHOULD minimize deeply nested object structures.
Preferred patterns:
- Use references to separate resources (IDs or links) instead of embedding full graphs.
- If you embed related data, embed only a small, stable summary (e.g.,
{id, name}), not the full related object.
When nesting is acceptable:
- Small, intrinsic value objects that are not meaningful as standalone resources (e.g.,
address,coordinates). - Stable summary/reference shapes.
Anti-patterns:
- Deeply nested graphs where changes to one domain object ripple into many responses.
- Embedding large sub-objects that make pagination/filtering/patching difficult.
Example (preferred):
{
"id": "123",
"ownerRef": { "id": "U1", "name": "Ada" }
}Anti-example: embedding entire related graphs several levels deep.
#709 - Define Defaults for Optional Properties
Intent: remove ambiguity when clients omit optional fields.
Optional properties MUST have documented default behavior when omitted.
Guidance:
- If a default is truly stable, declare it in OpenAPI using
default. - If a default is contextual (depends on tenant config, caller, feature flags), document that explicitly in prose (do not pretend it is constant).
- Avoid "magic defaults" that vary unpredictably across environments.
Boolean-specific guidance:
- Every optional boolean property MUST have a documented default behavior that applies when the property is absent.
- Booleans MUST never be
null(see [#700]). - If omitted, the server MUST behave as if the default value was supplied.
- Document the default in OpenAPI (use
defaultwhen it is truly stable) and in prose (especially if default is contextual).
Examples:
includeDisabled(optional): default isfalse.count(optional): default isfalse.
Testability:
- Reviewers can determine what happens when an optional field is omitted.
- Reviewers can determine the default behavior for every optional boolean.
#710 - Define Required Fields per OpenAPI
Intent: ensure clients and tooling know which inputs are mandatory.
APIs MUST model requiredness explicitly in OpenAPI.
Rules:
- Request bodies and parameters MUST mark required inputs using OpenAPI
required(properties) andrequired: true(parameters). - Responses SHOULD also be modeled accurately: if a field is always present, mark it required; if not always present, do not mark it required.
Create vs update:
- Do not overload one schema for every operation if requiredness differs.
- Use separate schemas (e.g.,
CreateAccountRequestvsUpdateAccountRequest) when the set of required fields differs.
OpenAPI example:
type: object
required: [id, name]
properties:
id:
type: string
name:
type: stringTestability:
- A reviewer can generate SDKs/validators that correctly enforce required inputs.
- Required fields in the spec match actual server behavior.
Boolean & Null Handling
#700 - Handle Null Values Correctly
Intent: avoid confusion, simplify client code, and ensure schemas match runtime behavior.
This rule defines how to handle null values across different data types and contexts.
Part 1: Booleans
Boolean properties MUST never be null.
Rules:
- If a boolean is required, it MUST be present and MUST be
trueorfalse. - If a boolean is optional and not applicable, omit it (do not send
null). - If you truly need three states, use an explicit enum (e.g.,
"ENABLED" | "DISABLED" | "UNKNOWN") and document the semantics.
Examples:
- ✅
{ "enabled": true } - ✅
{}(ifenabledis optional and not applicable) - ❌
{ "enabled": null }
Part 2: Arrays
Empty collections MUST be represented as empty arrays ([]), not null.
Examples:
- ✅
{ "items": [] } - ❌
{ "items": null }
Notes:
- If the property itself is optional, omission is allowed; but if the property is present, it must be an array.
Part 3: Null vs Absent Semantics
Default policy:
- Treat
nulland "absent" as equivalent for optional properties unless you explicitly document a different meaning. - Prefer omitting optional properties over sending
null. - If
nullis allowed, it must not change semantics relative to omission.
Boolean-specific guidance:
- If a boolean is not applicable, omit it rather than returning
null(see Part 1).
Examples:
- Response payloads: prefer
{}over{ "field": null }when a field is not present. - Request payloads: avoid overloading
nullvs absent to mean different things (e.g., "clear this value") unless the API explicitly defines that contract.
If you need "clear" semantics:
- Do not overload
nullvs absent informally. - Prefer explicit PATCH semantics (e.g., JSON Patch) or a documented request contract that clearly defines how values are cleared.
Part 4: OpenAPI Modeling
If an API can legitimately return or accept null for a field, the OpenAPI schema MUST model that.
Rules:
- If
nullis allowed at runtime, it must be modeled. - If
nullis not modeled, it MUST NOT appear at runtime. - Prefer omitting optional fields rather than returning
null(see Part 3). - Booleans MUST never be nullable (see Part 1).
OpenAPI guidance:
- For OpenAPI 3.0.x: use
nullable: true. - For OpenAPI 3.1.x: use JSON Schema unions (e.g.,
type: [string, "null"]).
Example (OAS 3.0):
middleName:
type: string
nullable: trueTestability:
- If responses contain
null, the spec reflects it. - If the spec does not mark nullable, responses do not contain
nullfor that field. - Booleans never appear as
nullin responses. - Arrays never appear as
null; empty arrays are used instead.