When an API returns data, the client expects a predictable structure: clear field names, readable dates, and definite rules for missing values. Without conventions, every team invents its own format — and integration turns into a quest to guess what null means in a given field.
In this article we go through concrete rules: how to name fields, how to return dates, when to return 201 and when 204, and why null in a response is a problem.
Field names: camelCase, not snake_case
Two camps have formed on the web: some APIs use created_at and order_id, others use createdAt and orderId. For a JSON API on Java the right choice is camelCase, because:
- JavaScript (the main consumer of REST APIs) uses camelCase by default;
- Jackson (the standard JSON library in Spring) writes field names as they appear in Java by default — and Java uses camelCase.
A good example of a JSON response:
{
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"totalAmount": 1500.00,
"status": "IN_PROGRESS",
"createdAt": "2026-05-26T10:30:00Z",
"deliveryAddress": {
"streetName": "Lenina",
"zipCode": "123456"
},
"items": [
{ "itemId": "abc123", "productName": "Keyboard", "quantity": 2 }
]
}
A few naming rules:
- Identifiers — with an
Idsuffix:orderId,customerId,parentCategoryId. A bareidis unclear — an identifier of what? - Dates — a string in ISO 8601 format:
2026-05-26for a date,2026-05-26T10:30:00Zfor a point in time (withZfor UTC). No2026-05-26 10:30:00without theTletter — that is not the standard. - Collections — plural:
items,tags,errors. - Enum values —
UPPER_SNAKE_CASE:IN_PROGRESS,CREDIT_CARD,OUT_OF_STOCK. The client immediately sees that it is an enumeration.
Boolean fields
There is no strict requirement to write isActive or active — both variants occur. Only one thing matters: consistency within the project. If you chose active/enabled — use it everywhere; if isActive/isEnabled — also everywhere.
{
"active": true,
"hasDiscount": false,
"canCancel": true
}
Dates and time: ISO 8601
Always use ISO 8601. It is an international standard that every library in every language understands.
- Date:
"2026-05-26" - Date and time (UTC):
"2026-05-26T10:30:00Z" - Date and time with offset:
"2026-05-26T13:30:00+03:00"
A common mistake is passing a Unix timestamp as a number (1716720600). It is machine-readable, but inconvenient when debugging and not obvious to the client. An ISO 8601 string is human-readable and at the same time parses just as well.
The response format depends on the operation
Different operations — different response codes and different body structures.
Create (POST) — 201 + Location + body
When a resource is created, the server returns status 201 Created and a Location header with a link to the created resource. The body contains the created object itself:
HTTP/1.1 201 Created
Location: /api/v1/orders/550e8400-e29b-41d4-a716-446655440000
{
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"status": "CREATED",
"totalAmount": 0,
"createdAt": "2026-05-26T10:30:00Z"
}
Why Location? The client immediately knows the URL of the new resource, without having to guess or construct it.
Update (PUT/PATCH) — 200 + updated resource
After an update we return the current state of the object:
HTTP/1.1 200 OK
{
"orderId": "550e8400-...",
"status": "CONFIRMED",
"totalAmount": 1500.00,
"updatedAt": "2026-05-26T11:00:00Z"
}
The client immediately sees what changed — without an additional GET request.
Delete (DELETE) — 204 No Content
A delete returns nothing. Status 204 No Content, empty body:
HTTP/1.1 204 No Content
There is no need to return { "success": true } — status 204 already signals success.
Action on a resource (action) — 200 + result
If the endpoint is an action (confirm an order, block a user), we return the updated resource:
POST /api/v1/orders/550e8400-.../confirm
HTTP/1.1 200 OK
{
"orderId": "550e8400-...",
"status": "CONFIRMED",
"confirmedAt": "2026-05-26T11:00:00Z"
}
Single resource — a flat object
No wrappers. Just the object:
{
"orderId": "550e8400-...",
"status": "CONFIRMED",
"totalAmount": 1500.00,
"createdAt": "2026-05-26T10:30:00Z"
}
Nested objects and arrays inside a resource are fine. But do not wrap the resource in { "data": ..., "success": true } — that is an antipattern, more on it below.
Collection — content + pagination metadata
When you return a paginated list, the structure is as follows:
{
"content": [
{ "orderId": "..." },
{ "orderId": "..." }
],
"page": 1,
"size": 20,
"totalElements": 243,
"totalPages": 13
}
The content field is the data itself, alongside it the pagination metadata. This is not a "wrapper" in the bad sense, it is the structure of a page.
null in a response — why it is a problem
When a client receives null in a field, it does not know what it means: is there no data at all? Not loaded yet? Was there, but deleted? Every null value is uncertainty.
The rule is simple: if a field has no value — it should not be in the JSON at all, rather than "discount": null.
Bad:
{
"orderId": "...",
"discount": null,
"comment": null
}
Good:
{
"orderId": "...",
"status": "CONFIRMED"
}
More upsides: less traffic, simpler client code (if (data.discount) instead of if (data.discount !== null && data.discount !== undefined)).
In Spring this is configured with a single line — we tell Jackson not to include null fields:
@Bean
public ObjectMapper objectMapper() {
return Jackson2ObjectMapperBuilder.json()
.serializationInclusion(JsonInclude.Include.NON_NULL)
.build();
}
The same logic applies to empty strings: "" is not "no data", it is "there is data, but it is empty". If there is no data — the field is not in the JSON.
null in a PATCH request body — a different story
In PATCH requests null has a special meaning according to the JSON Merge Patch standard (RFC 7396): it is a command to remove the field.
PATCH /api/v1/orders/550e8400-...
Content-Type: application/merge-patch+json
{ "comment": null }
This says: "remove the comment field from the resource". This is request semantics, not a violation of the rule about null in responses.
Envelope — an antipattern
Sometimes you see a response format like this:
{
"success": true,
"data": {
"orderId": "...",
"status": "CREATED"
},
"error": null
}
This is called an envelope (a "wrapper"). It seems convenient — always the same structure. But in practice it is an extra layer with no benefit:
- The HTTP status (200, 404, 500) already reports whether it was a success or an error — duplicating it in
"success": trueis pointless. - For errors there is a separate standard (RFC 9457) that describes the problem better.
- The client writes
response.data.orderIdinstead ofresponse.orderId— an extra level.
The right way: a single resource is returned as a flat object, a collection — via { "content": [...] } with pagination.
Empty collections — [] not null
If a collection is empty — return an empty array, not null and not a missing field:
{ "items": [] }
[] means "the collection exists, there are no elements". null or a missing field is ambiguous: is the collection empty, does it not exist, or is it not loaded?
In short
- Field names — camelCase:
orderId,totalAmount,createdAt. - Identifiers — an
Idsuffix:orderId,customerId. - Dates — a string in ISO 8601:
2026-05-26T10:30:00Z. - Enums — UPPER_SNAKE_CASE:
IN_PROGRESS,CREDIT_CARD. - Create — 201 Created + a
Locationheader + the resource body. - Update — 200 OK + the updated resource.
- Delete — 204 No Content, empty body.
- Single resource — a flat object, no wrappers.
- Paginated collection —
{ "content": [...], "page": ..., "totalElements": ... }. nullin a 2xx response is forbidden — if there is no data, the field is not in the JSON.nullin a PATCH body — a special "remove the field" command (JSON Merge Patch).- Empty collections —
[], notnull.
What to read next
- URL and Resources in REST API — HTTP methods, status codes, URL structure.
- Query Parameters and Pagination — how to pass filters and get pages.
- Errors and the Problem Response — the RFC 9457 error format.
- Headers and Tracing — Location, ETag and service headers.