API Validation Errors
FIELD-LEVEL VALIDATION ERROR DESIGN — RETURNING ALL VALIDATION FAILURES IN A SINGLE RESPONSE WITH JSON POINTER PATHS AND PER-FIELD MESSAGES ELIMINATES THE ONE-ERROR-AT-A-TIME DEBUGGING LOOP AND GIVES CLIENTS ENOUGH INFORMATION TO HIGHLIGHT EVERY INVALID FIELD WITHOUT A SECOND REQUEST.
When to Use
- Designing the validation error response for a form submission, resource creation, or bulk import endpoint
- Reviewing a PR that returns
400 with a single error message for a request that may have multiple invalid fields
- Choosing between
400 Bad Request and 422 Unprocessable Entity for semantic validation failures
- Implementing field-level error display in a client application that consumes API validation responses
- Building an API that mirrors JSON:API error object conventions or RFC 9457 validation extensions
- Documenting the validation error schema for an OpenAPI specification
- Designing error responses for a nested resource where invalid fields may be deep in the payload hierarchy
- Auditing an existing API whose clients report that form validation requires multiple round-trips to surface all errors
Instructions
Key Concepts
Multi-field error arrays — A single validation response should report all failing fields simultaneously, not just the first one encountered. The response body includes an array of error objects, each describing one invalid field: "errors": [{ "pointer": "/email", ... }, { "pointer": "/birthdate", ... }]. Stopping at the first failure creates a "whack-a-mole" experience where callers must submit, fail, fix, and resubmit for each field in turn.
JSON Pointer (RFC 6901) — A standardized syntax for identifying a specific value within a JSON document. Pointers use / as a separator: /user/email identifies the email field inside a user object; /items/0/price identifies the price of the first element in an items array. In validation error responses, the pointer (or source.pointer in JSON:API) field identifies exactly which part of the request body failed validation — no ambiguity, no path string parsing.
source/pointer vs source/parameter — JSON:API distinguishes two sources of validation error:
"source": { "pointer": "/data/attributes/email" } — the error is in the request body, at a JSON Pointer location.
"source": { "parameter": "filter[status]" } — the error is in a query parameter, not the body.
Use pointer for body fields, parameter for query string inputs. RFC 9457 extensions use "pointer" directly as a top-level extension field rather than nesting under source.
422 vs 400 — Use 400 Bad Request for structurally malformed requests: unparseable JSON, missing Content-Type, invalid URL path parameters. Use 422 Unprocessable Entity for requests that are syntactically valid but semantically invalid: a correctly parsed JSON body where email is not an email address, end_date precedes start_date, or a required field is present but empty. The distinction matters because 422 tells the client "your request reached the validation layer and failed there" — it is never retryable without changing the payload.
Per-field titles and details — Each error object in the array should include a stable title (the validation rule that failed: "Must be a valid email address") and an instance-specific detail ("'not-an-email' is not a valid email address format"). The title is reusable across occurrences of the same rule; detail adds the specific value that failed, making it debuggable without inspecting the original request.
Worked Example
A Stripe-style account creation endpoint returning multi-field validation errors:
Request with multiple invalid fields:
POST /v1/accounts
Authorization: Bearer sk_test_...
Content-Type: application/json
{
"email": "not-an-email",
"country": "XX",
"business_type": "individual",
"individual": {
"dob": {
"day": 32,
"month": 13,
"year": 1850
}
}
}
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/validation-failed",
"title": "Validation Failed",
"status": 422,
"detail": "3 fields failed validation. Correct the highlighted fields and resubmit.",
"instance": "/errors/correlation/a1b2-c3d4",
"errors": [
{
"pointer": "/email",
"title": "Must be a valid email address",
"detail": "'not-an-email' does not match the expected email format."
},
{
"pointer": "/country",
"title": "Must be a valid ISO 3166-1 alpha-2 country code",
"detail": "'XX' is not a recognized country code."
},
{
"pointer": "/individual/dob/day",
"title": "Day must be between 1 and 31",
"detail": "Received 32. Days in a month range from 1 to 31."
}
]
}
The pointer paths use RFC 6901 syntax: /email addresses the top-level field; /individual/dob/day drills into the nested individual.dob.day path. A client rendering a form can use each pointer to highlight the exact input that failed without any string parsing.
Query parameter validation error (400 Bad Request):
GET /v1/payments?status=unknownstatus&limit=abc
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/invalid-query-parameter",
"title": "Invalid Query Parameter",
"status": 400,
"detail": "2 query parameters are invalid.",
"errors": [
{
"parameter": "status",
"title": "Must be one of: pending, succeeded, failed",
"detail": "'unknownstatus' is not a valid status value."
},
{
"parameter": "limit",
"title": "Must be an integer",
"detail": "'abc' cannot be parsed as an integer."
}
]
}
Query parameter errors use "parameter" instead of "pointer" because they are not in the request body.
Anti-Patterns
Returning a single error for the first failing field. A form with 5 invalid fields returns only the first error. The user fixes it, resubmits, receives the second error, and so on for 5 round-trips. Fix: validate the entire request body, collect all errors, and return the full list in a single 422 response.
Using vague path strings instead of RFC 6901 pointers. "field": "individual.dob.day" uses dot notation that requires parsing and breaks for array indices. "field": "items[0].price" uses a mix of dot and bracket notation with no standard. Fix: use RFC 6901 JSON Pointer syntax ("/individual/dob/day", "/items/0/price") — it is unambiguous, parseable by standard libraries, and consistent across implementations.
Returning 400 for semantic validation failures. A request body that is valid JSON but contains an email address string that fails the email format check is not malformed — it passed JSON parsing. Returning 400 mixes structural errors with semantic ones, complicating client error routing. Fix: reserve 400 for structural failures (unparseable JSON, wrong Content-Type) and use 422 for any failure that occurs after successful parsing and type coercion.
Omitting the pointer for nested fields. Returning { "field": "dob", "message": "Invalid date of birth" } for a nested field fails to identify which level of nesting failed, and whether dob.day, dob.month, or dob.year is the problem. Fix: use the full JSON Pointer path to the failing field, however deep it is in the payload.
Details
JSON Pointer Encoding
RFC 6901 defines two escape sequences for characters that conflict with the pointer syntax: ~0 represents a literal ~, and ~1 represents a literal /. If a field name contains a slash — e.g., "Content-Type" — the pointer is /Content~1Type. This is rare in practice but important when generating pointers programmatically from field names.
Validation Error Design for Arrays
For bulk operations or array inputs, the pointer must include the array index: /items/2/quantity identifies the quantity field of the third element (zero-indexed) in the items array. This is essential for bulk import endpoints where clients need to know which rows failed without re-matching errors to rows by field name.
Real-World Case Study: Shopify GraphQL Validation Errors
Shopify's Admin API (both REST and GraphQL) returns structured validation errors with field paths. In the REST API, errors follow a { "errors": { "field_name": ["message"] } } shape. In the GraphQL API, errors use the userErrors pattern: { "userErrors": [{ "field": ["lineItems", "0", "quantity"], "message": "Quantity must be greater than zero" }] }. The field array is equivalent to a JSON Pointer path split on /. Shopify's developer documentation shows that APIs returning structured field-path errors report significantly fewer "which field caused the error?" support questions than APIs returning only top-level messages. The field path is the minimum information needed for a client to display inline validation feedback without guessing.
Source
Process
- Identify all inputs that require validation: request body fields, path parameters, query parameters, and headers.
- Run validation against all fields and collect the complete error list before constructing the response.
- Map each error to a
pointer (for body fields using RFC 6901) or parameter (for query string inputs).
- Construct the
422 response with an errors array containing per-field title and detail entries.
- Run
harness validate to confirm skill files are well-formed and cross-references are correct.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
- related_skills: api-problem-details-rfc, api-error-contracts, api-bulk-operations, api-status-codes
Success Criteria
- Validation responses include all failing fields in a single response, never just the first one.
- Field paths use RFC 6901 JSON Pointer syntax (
/field/subfield/index), not dot notation or custom path formats.
422 Unprocessable Entity is used for semantic validation failures; 400 Bad Request is reserved for structural/parse failures.
- Each error object includes a stable
title (the rule) and an instance-specific detail (the offending value and why it failed).
- Query parameter errors use a
parameter field, not a pointer field.
1---2name: api-validation-errors3description: API Validation Errors4---5# API Validation Errors67> FIELD-LEVEL VALIDATION ERROR DESIGN — RETURNING ALL VALIDATION FAILURES IN A SINGLE RESPONSE WITH JSON POINTER PATHS AND PER-FIELD MESSAGES ELIMINATES THE ONE-ERROR-AT-A-TIME DEBUGGING LOOP AND GIVES CLIENTS ENOUGH INFORMATION TO HIGHLIGHT EVERY INVALID FIELD WITHOUT A SECOND REQUEST.89## When to Use1011- Designing the validation error response for a form submission, resource creation, or bulk import endpoint12- Reviewing a PR that returns `400` with a single error message for a request that may have multiple invalid fields13- Choosing between `400 Bad Request` and `422 Unprocessable Entity` for semantic validation failures14- Implementing field-level error display in a client application that consumes API validation responses15- Building an API that mirrors JSON:API error object conventions or RFC 9457 validation extensions16- Documenting the validation error schema for an OpenAPI specification17- Designing error responses for a nested resource where invalid fields may be deep in the payload hierarchy18- Auditing an existing API whose clients report that form validation requires multiple round-trips to surface all errors1920## Instructions2122### Key Concepts23241. **Multi-field error arrays** — A single validation response should report all failing fields simultaneously, not just the first one encountered. The response body includes an array of error objects, each describing one invalid field: `"errors": [{ "pointer": "/email", ... }, { "pointer": "/birthdate", ... }]`. Stopping at the first failure creates a "whack-a-mole" experience where callers must submit, fail, fix, and resubmit for each field in turn.25262. **JSON Pointer (RFC 6901)** — A standardized syntax for identifying a specific value within a JSON document. Pointers use `/` as a separator: `/user/email` identifies the `email` field inside a `user` object; `/items/0/price` identifies the `price` of the first element in an `items` array. In validation error responses, the `pointer` (or `source.pointer` in JSON:API) field identifies exactly which part of the request body failed validation — no ambiguity, no path string parsing.27283. **`source/pointer` vs `source/parameter`** — JSON:API distinguishes two sources of validation error:29 - `"source": { "pointer": "/data/attributes/email" }` — the error is in the request body, at a JSON Pointer location.30 - `"source": { "parameter": "filter[status]" }` — the error is in a query parameter, not the body.31 Use `pointer` for body fields, `parameter` for query string inputs. RFC 9457 extensions use `"pointer"` directly as a top-level extension field rather than nesting under `source`.32334. **422 vs 400** — Use `400 Bad Request` for structurally malformed requests: unparseable JSON, missing `Content-Type`, invalid URL path parameters. Use `422 Unprocessable Entity` for requests that are syntactically valid but semantically invalid: a correctly parsed JSON body where `email` is not an email address, `end_date` precedes `start_date`, or a required field is present but empty. The distinction matters because `422` tells the client "your request reached the validation layer and failed there" — it is never retryable without changing the payload.34355. **Per-field titles and details** — Each error object in the array should include a stable `title` (the validation rule that failed: `"Must be a valid email address"`) and an instance-specific `detail` (`"'not-an-email' is not a valid email address format"`). The `title` is reusable across occurrences of the same rule; `detail` adds the specific value that failed, making it debuggable without inspecting the original request.3637### Worked Example3839A Stripe-style account creation endpoint returning multi-field validation errors:4041**Request with multiple invalid fields:**4243```http44POST /v1/accounts45Authorization: Bearer sk_test_...46Content-Type: application/json4748{49 "email": "not-an-email",50 "country": "XX",51 "business_type": "individual",52 "individual": {53 "dob": {54 "day": 32,55 "month": 13,56 "year": 185057 }58 }59}60```6162```http63HTTP/1.1 422 Unprocessable Entity64Content-Type: application/problem+json6566{67 "type": "https://api.example.com/errors/validation-failed",68 "title": "Validation Failed",69 "status": 422,70 "detail": "3 fields failed validation. Correct the highlighted fields and resubmit.",71 "instance": "/errors/correlation/a1b2-c3d4",72 "errors": [73 {74 "pointer": "/email",75 "title": "Must be a valid email address",76 "detail": "'not-an-email' does not match the expected email format."77 },78 {79 "pointer": "/country",80 "title": "Must be a valid ISO 3166-1 alpha-2 country code",81 "detail": "'XX' is not a recognized country code."82 },83 {84 "pointer": "/individual/dob/day",85 "title": "Day must be between 1 and 31",86 "detail": "Received 32. Days in a month range from 1 to 31."87 }88 ]89}90```9192The `pointer` paths use RFC 6901 syntax: `/email` addresses the top-level field; `/individual/dob/day` drills into the nested `individual.dob.day` path. A client rendering a form can use each `pointer` to highlight the exact input that failed without any string parsing.9394**Query parameter validation error (400 Bad Request):**9596```http97GET /v1/payments?status=unknownstatus&limit=abc98```99100```http101HTTP/1.1 400 Bad Request102Content-Type: application/problem+json103104{105 "type": "https://api.example.com/errors/invalid-query-parameter",106 "title": "Invalid Query Parameter",107 "status": 400,108 "detail": "2 query parameters are invalid.",109 "errors": [110 {111 "parameter": "status",112 "title": "Must be one of: pending, succeeded, failed",113 "detail": "'unknownstatus' is not a valid status value."114 },115 {116 "parameter": "limit",117 "title": "Must be an integer",118 "detail": "'abc' cannot be parsed as an integer."119 }120 ]121}122```123124Query parameter errors use `"parameter"` instead of `"pointer"` because they are not in the request body.125126### Anti-Patterns1271281. **Returning a single error for the first failing field.** A form with 5 invalid fields returns only the first error. The user fixes it, resubmits, receives the second error, and so on for 5 round-trips. Fix: validate the entire request body, collect all errors, and return the full list in a single `422` response.1291302. **Using vague path strings instead of RFC 6901 pointers.** `"field": "individual.dob.day"` uses dot notation that requires parsing and breaks for array indices. `"field": "items[0].price"` uses a mix of dot and bracket notation with no standard. Fix: use RFC 6901 JSON Pointer syntax (`"/individual/dob/day"`, `"/items/0/price"`) — it is unambiguous, parseable by standard libraries, and consistent across implementations.1311323. **Returning `400` for semantic validation failures.** A request body that is valid JSON but contains an email address string that fails the email format check is not malformed — it passed JSON parsing. Returning `400` mixes structural errors with semantic ones, complicating client error routing. Fix: reserve `400` for structural failures (unparseable JSON, wrong Content-Type) and use `422` for any failure that occurs after successful parsing and type coercion.1331344. **Omitting the `pointer` for nested fields.** Returning `{ "field": "dob", "message": "Invalid date of birth" }` for a nested field fails to identify which level of nesting failed, and whether `dob.day`, `dob.month`, or `dob.year` is the problem. Fix: use the full JSON Pointer path to the failing field, however deep it is in the payload.135136## Details137138### JSON Pointer Encoding139140RFC 6901 defines two escape sequences for characters that conflict with the pointer syntax: `~0` represents a literal `~`, and `~1` represents a literal `/`. If a field name contains a slash — e.g., `"Content-Type"` — the pointer is `/Content~1Type`. This is rare in practice but important when generating pointers programmatically from field names.141142### Validation Error Design for Arrays143144For bulk operations or array inputs, the `pointer` must include the array index: `/items/2/quantity` identifies the `quantity` field of the third element (zero-indexed) in the `items` array. This is essential for bulk import endpoints where clients need to know which rows failed without re-matching errors to rows by field name.145146### Real-World Case Study: Shopify GraphQL Validation Errors147148Shopify's Admin API (both REST and GraphQL) returns structured validation errors with field paths. In the REST API, errors follow a `{ "errors": { "field_name": ["message"] } }` shape. In the GraphQL API, errors use the `userErrors` pattern: `{ "userErrors": [{ "field": ["lineItems", "0", "quantity"], "message": "Quantity must be greater than zero" }] }`. The `field` array is equivalent to a JSON Pointer path split on `/`. Shopify's developer documentation shows that APIs returning structured field-path errors report significantly fewer "which field caused the error?" support questions than APIs returning only top-level messages. The field path is the minimum information needed for a client to display inline validation feedback without guessing.149150## Source151152- [JSON:API — Error Objects](https://jsonapi.org/format/#error-objects)153- [RFC 6901 — JavaScript Object Notation (JSON) Pointer](https://rfc-editor.org/rfc/rfc6901)154- [RFC 9457 — Problem Details for HTTP APIs](https://rfc-editor.org/rfc/rfc9457)155- [Shopify API — Error Handling](https://shopify.dev/docs/api/usage/response-codes)156- [APIs You Won't Hate — Validation Errors](https://apisyouwonthate.com/blog/rest-api-error-choose-your-own-adventure)157158## Process1591601. Identify all inputs that require validation: request body fields, path parameters, query parameters, and headers.1612. Run validation against all fields and collect the complete error list before constructing the response.1623. Map each error to a `pointer` (for body fields using RFC 6901) or `parameter` (for query string inputs).1634. Construct the `422` response with an `errors` array containing per-field `title` and `detail` entries.1645. Run `harness validate` to confirm skill files are well-formed and cross-references are correct.165166## Harness Integration167168- **Type:** knowledge — this skill is a reference document, not a procedural workflow.169- **No tools or state** — consumed as context by other skills and agents.170- **related_skills:** api-problem-details-rfc, api-error-contracts, api-bulk-operations, api-status-codes171172## Success Criteria173174- Validation responses include all failing fields in a single response, never just the first one.175- Field paths use RFC 6901 JSON Pointer syntax (`/field/subfield/index`), not dot notation or custom path formats.176- `422 Unprocessable Entity` is used for semantic validation failures; `400 Bad Request` is reserved for structural/parse failures.177- Each error object includes a stable `title` (the rule) and an instance-specific `detail` (the offending value and why it failed).178- Query parameter errors use a `parameter` field, not a `pointer` field.