HTTP methods are a contract, not a suggestion
Every HTTP request declares an intent through its method: read, create, replace, partially update, or delete. JSON is how the data for that intent gets carried — in the request body, the response body, or both. Using the right method, with the right JSON shape, is what makes a REST API predictable to every client that calls it, from a browser fetch to a generated SDK.
This guide walks through GET, POST, PUT, PATCH and DELETE, exactly how JSON fits each one, and the two properties — safe and idempotent — that determine whether a client (or a proxy, or a retry policy) can call a method again without asking permission first.
Safe and idempotent, defined once
Two words come up constantly in this space and are worth pinning down before the examples:
- Safe — the method doesn't change server state. A client (or a search engine crawler, or a CDN) can call it freely without side effects.
- Idempotent — calling it once or a hundred times with the same input leaves the server in the same state as calling it once. This is what makes automatic retries safe.
{
"GET": { "safe": true, "idempotent": true },
"PUT": { "safe": false, "idempotent": true },
"DELETE": { "safe": false, "idempotent": true },
"PATCH": { "safe": false, "idempotent": false },
"POST": { "safe": false, "idempotent": false }
}Notice PATCH and POST are the two methods that are not idempotent by default — retrying them can create a second resource or apply the same partial update twice. That's exactly why idempotency keys exist for POST (see JSONKit's guide on idempotency in JSON APIs) — the HTTP spec doesn't give POST that safety for free.
GET — read, no request body
GET fetches a resource. It should never carry a JSON request body — some clients and proxies silently strip it, so query parameters are the correct place for filters, not a body.
GET /orders/ord_42{
"id": "ord_42",
"status": "shipped",
"total": 149.97,
"items": [{ "productId": "PROD-42", "quantity": 1 }]
}For filtering a list, use query parameters — never a body — because GET requests must remain safe and cacheable:
GET /orders?status=shipped&limit=20POST — create, JSON body, not idempotent
POST creates a new resource (or triggers an action that doesn't map cleanly to a resource, like sending an email). The request carries a JSON body describing what to create; a successful response is 201 Created with the new resource, including its server-assigned id.
POST /orders
Content-Type: application/json
{ "items": [{ "productId": "PROD-42", "quantity": 1 }] }201 Created
Location: /orders/ord_43
{ "id": "ord_43", "status": "pending", "items": [{ "productId": "PROD-42", "quantity": 1 }] }Because POST is not idempotent, calling it twice — say, after a network timeout hides the first response — can create two orders. That's the exact problem an Idempotency-Key header solves: the server recognizes the retry and returns the original order instead of creating a duplicate.
PUT — full replace, JSON body, idempotent
PUT replaces a resource entirely with the JSON you send. The body must represent the *whole* resource — any field you omit is typically treated as absent or reset, not left alone. Calling the same PUT twice produces the same end state, which is what makes it idempotent.
PUT /orders/ord_43
Content-Type: application/json
{ "status": "shipped", "items": [{ "productId": "PROD-42", "quantity": 1 }], "trackingNumber": "DLVY99" }200 OK
{ "id": "ord_43", "status": "shipped", "items": [{ "productId": "PROD-42", "quantity": 1 }], "trackingNumber": "DLVY99" }If you PUT a body missing trackingNumber, a strict full-replace implementation drops it — which is exactly why PUT is the wrong tool for updating a single field on a large resource.
PATCH — partial update, JSON body, not idempotent by default
PATCH updates *part* of a resource — you send only the fields that change. Unlike PUT, PATCH is not guaranteed idempotent by the spec, though many APIs implement it idempotently in practice by treating each field as a "set to this value" instruction rather than a running change.
The simplest and most common convention is JSON Merge Patch (RFC 7396): send an object with only the changed keys, and null explicitly removes a field.
PATCH /orders/ord_43
Content-Type: application/merge-patch+json
{ "status": "delivered", "trackingNumber": null }200 OK
{ "id": "ord_43", "status": "delivered", "items": [{ "productId": "PROD-42", "quantity": 1 }] }Only status changed and trackingNumber was removed; every other field — including items, which wasn't mentioned — stayed exactly as it was. That "only touch what's mentioned" behavior is precisely what PUT does not give you.
For more surgical operations — insert into the middle of an array, test-then-set, move a value — JSON Patch (RFC 6902) sends an array of explicit operations instead of a merged object:
[
{ "op": "replace", "path": "/status", "value": "delivered" },
{ "op": "remove", "path": "/trackingNumber" }
]DELETE — remove, usually no body, idempotent
DELETE removes a resource. It typically carries no request body, and a successful response is either 204 No Content (nothing to return) or 200 OK with a confirmation body. It's idempotent because deleting an already-deleted resource has the same end state as deleting it once — most APIs return 204 (or 404) either way, rather than erroring on the second call.
DELETE /orders/ord_43204 No ContentSide-by-side
| Method | Request body | Idempotent | Safe | Typical success |
|---|---|---|---|---|
| GET | None | Yes | Yes | 200 OK |
| POST | Full resource to create | No | No | 201 Created |
| PUT | Full resource (replace) | Yes | No | 200 OK |
| PATCH | Partial change (merge patch or ops) | Not guaranteed | No | 200 OK |
| DELETE | Usually none | Yes | No | 204 No Content |
A newer method for safe queries with a body: HTTP QUERY
Earlier in this guide, GET's "no request body" rule came up as a real limitation: some reads genuinely need a complex filter — a GraphQL-style query, a large list of IDs, a multi-field search — that doesn't fit cleanly into a query string. Historically, teams worked around this by abusing POST for what is really a read, which throws away GET's safety and cacheability.
QUERY is a newer HTTP method, standardized through the IETF (draft-ietf-httpbis-safe-method-w-body), created specifically to close this gap: a method that is safe like GET — it doesn't change server state, and it's cacheable in principle — but that's explicitly allowed to carry a JSON request body, the way POST does.
QUERY /orders
Content-Type: application/json
Accept: application/json
{ "filter": { "status": "shipped", "total": { "gte": 100 } }, "sort": "-createdAt", "limit": 20 }200 OK
{ "data": [ { "id": "ord_43", "status": "shipped", "total": 149.97 } ], "hasMore": false }The response body and status codes work exactly like a GET response — this is a read, not a write.
Where it fits, and its current status
QUERY is aimed squarely at search-heavy and filter-heavy endpoints: complex list filters, GraphQL-like read queries, and any "read with a large or structured input" case that today gets forced onto POST. As of this writing it's still a draft standard — support is landing incrementally across HTTP servers, frameworks and proxies rather than being universally available, so check that your specific stack (server framework, load balancer, API gateway, client library) actually understands the QUERY method before relying on it in production. Until then, the practical options remain: GET with query parameters for simple filters, or POST for anything that needs a body but doesn't have official read semantics yet.
Choosing PUT vs PATCH in practice
Use PUT when the client naturally has the whole resource already — a settings form that always submits every field, or a client that fetched the full object, edited it, and is writing it back. Use PATCH when the client only knows about one field it wants to change — toggling a single boolean, updating a status — without needing to fetch and resend everything else first. Sending a PUT with a partial body to an API that does a true full-replace is a classic bug: fields you didn't include quietly disappear.
Where JSON Schema fits
Whatever method is in play, the JSON body is exactly what a JSON Schema describes and a validator like JSONKit's JSON Schema Validator checks: required fields for a POST body, the full resource shape for a PUT, and — with PATCH — a schema where every property is optional, since any subset can legitimately be sent. Modeling PATCH bodies with JSON Schema is worth doing deliberately: mark nothing as required, and note that null on merge-patch payloads means "delete this field," not "invalid value."