skip to content

Validation Error Shapes

Reporting request-validation failures so clients can render them: field-level error arrays, pointers into the request body, and collecting every violation in one response. Interviewers ask because the 400-vs-422 call and per-field shapes come up in every form-backed API.

part ofAPI stylesoverview, primer and where to startread it →
on this pageshow

questions

4

A client posts a JSON body to your REST API and several fields fail validation. How would you shape the response body so the caller can highlight the exact inputs that failed, rather than returning one human-readable sentence?

level: juniorimportance: must knowfreq 62%

answer

  1. array of violations, not a sentence
  2. target + code + message (+ params)
  3. code is the contract, message is not
  4. one well-known key, one generic handler
  5. no regexes, SQL or echoed secrets

basics

~20 s

Return an array of violations, one per failed field. Each entry carries a machine-readable code, a target naming the field, and a human message. Clients switch on the code and attach the message to the right input; a prose string cannot be parsed.

solid answer

~40 s

I return a list, not a sentence. Each violation has three parts: a target (which field failed, as a name or a JSON Pointer like /items/3/quantity), a stable code the client can branch on (required, range.min, format.email), and a message for humans. I add a params object ({"min": 1}) so a client that localises its own text can rebuild the sentence. The code is the contract; message wording is a courtesy and may change any release. All violations sit under one well-known key - errors or violations - inside whatever error envelope the API already uses, so a single generic handler finds them everywhere. Cross-field rules such as end-date-after-start-date go in the same array, targeting the object that owns the rule.

code

json · 7 lines
json
{
  "errors": [
    { "target": "/email", "code": "format.email", "message": "must be a valid email address" },
    { "target": "/items/3/quantity", "code": "range.min", "message": "must be at least 1", "params": { "min": 1 } },
    { "target": "/shippingAddress/postalCode", "code": "required", "message": "must not be null" }
  ]
}

go deeper

for a junior

Name the three parts - field target, code, message - and show a small array. That is the whole expected answer.

for a middle

Add params for localisation, explain why the code is the stable contract and the message is not, and cover errors that belong to no single field.

for a senior

Talk about enforcing the shape centrally, code stability as a versioning obligation, capping array size, and not leaking internals.

for a principal

Frame it as an org-wide contract: one envelope every service emits, a shared client handler, additive-only code vocabulary, and conformance tests that fail a service emitting its own shape.

## Why one message is not enough A body like {"message": "Email is invalid and quantity must be at least 1"} is a dead end. A web form wants to outline the offending input and print text beneath it; a mobile client wants to scroll to the first bad field; a batch integration wants to log which record failed and why. All three need the failure decomposed. Parsing English is not an option, and the moment someone translates the sentence every client breaks. ## The three parts of a violation **Target** - which piece of the payload is at fault. A flat field name works for flat bodies; a JSON Pointer (RFC 6901) such as /items/3/quantity is the usual choice once bodies nest. Whatever you pick, it must name the wire representation the client sent, not an internal class field. **Code** - a short, stable, machine-comparable token: required, format.email, range.min, pattern, too-long, conflict.duplicate. This is what clients branch on. Treat it like an enum in your public contract: additive changes are fine, renames are breaking. **Message** - a human sentence for display or logs. Explicitly not stable, and never the client's control flow. A fourth part earns its place often: **params**, a small map of the rule's arguments ({"min":1,"max":99}). With code plus params a client can render its own localised copy without string surgery. ## One array, one place Put every violation in one array under a single agreed key. Clients then write one handler: if the response has errors[], iterate and bind each entry to a control. Splitting field errors across ad-hoc keys per endpoint (badEmail, quantityError) defeats the whole point. ## Things that are not field errors Some failures have no single owning field: an invalid combination of two fields, a body that violates a business invariant, or a header or query parameter that is wrong. Keep the same array and either target the containing object (empty pointer for the document root) or add a location discriminator (body, query, header, path) alongside the name. A parameter cannot be addressed by a JSON Pointer into the body, so a name-plus-location form is what query and header problems need. ## What not to leak Echo of raw input values, regexes, ORM or SQL fragments, and internal type names are all information disclosure and coupling. Say what rule failed, not how the server is built. Also cap the array; a machine-generated payload with ten thousand rows should not produce a ten-thousand-entry error body. ## Consistency beats cleverness The shape is only valuable if every endpoint uses it, so pin it once - in a shared exception handler or middleware - and make hand-rolled variants a review defect.

  • Who is responsible for translating the message - server or client?
    Either, but decide once. If the server translates it reads Accept-Language and returns a localised message, which keeps clients dumb but puts your copy in the API. If the client translates, the server must supply a stable code plus a params map so the client can interpolate its own string. What does not work is a client trying to localise a server sentence it cannot parse.
  • Should the field name in the error match the JSON property or the server-side field?
    Always the JSON property exactly as the client sent it, including case convention. If the server maps camelCase JSON onto snake_case entities or renames via annotations, the error target must be translated back to the wire name. Otherwise clients cannot map the violation to the input that produced it.

saying these in an interview costs you the question

  • Returning a single concatenated string and calling it machine-readable
  • Making clients match on the human message text because there is no code
  • Inventing a different error shape per endpoint
  • Echoing the rejected value or the failing regex back to the caller

context

open as a page

A request body is well-formed JSON and matches the declared content type, but breaks a schema or business rule. Would you answer with HTTP status 400 or HTTP status 422, and what distinction do those two codes actually carry?

level: middleimportance: must knowfreq 68%

basics

~20 s

400 means the request itself is malformed - bad syntax, unparseable JSON, missing required parameter. 422 means the syntax parsed fine but the content was semantically unprocessable. Both are non-retryable client errors; pick one convention and apply it API-wide.

open as a page

When a submitted request breaks a dozen rules at once, do you return the first failure or all of them in one response? What are the consequences of each choice for the caller and for the server?

level: middleimportance: should knowfreq 52%

basics

~20 s

Aggregate: run all cheap validators and return every violation in one array, so a form can be fixed in one pass instead of one round trip per mistake. Fail fast only across layers - parse errors stop everything, and expensive or stateful checks run only after cheap ones pass.

open as a page

In a validation error response, how do you identify the offending value when it sits deep inside the payload - say the quantity of the fourth element of an items array? What does an RFC 6901 JSON Pointer give you, and where does it fall short?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Use a JSON Pointer into the request document: /items/3/quantity. It is a slash-separated path of member names and zero-based array indexes, empty string for the whole document, with ~1 escaping a literal slash and ~0 a literal tilde. It cannot address query parameters or headers.

open as a page