skip to content

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