Validation Errors
Validation errors are the most frequent errors developers encounter during API integration. Every form submission, every data import, every automated workflow hits validation at some point. The quality of your validation error responses directly determines how fast developers can integrate with your API.
The fundamental rule: return ALL validation errors at once, not one at a time. A developer who submits a form with three invalid fields should not have to make three round trips to discover all three problems.
Field-Level Validation Errors
The standard pattern for validation errors returns an array of field-specific error objects:
{
"error": {
"code": "validation_error",
"message": "Request validation failed. See details for specific errors.",
"details": [
{
"field": "email",
"code": "invalid_format",
"message": "Must be a valid email address."
},
{
"field": "name",
"code": "too_short",
"message": "Must be at least 2 characters."
},
{
"field": "age",
"code": "out_of_range",
"message": "Must be between 13 and 150."
}
],
"request_id": "req_abc123"
}
}
Each error in the details array has three components:
field— the exact field name from the request body, matching the JSON pathcode— a machine-readable error code that clients can switch onmessage— a human-readable message suitable for displaying to end users
The field value must match the request field name exactly. If the request sends email_address, the error should reference email_address, not email.
Nested Field References
For nested objects, use dot notation or JSON pointer syntax:
{
"error": {
"code": "validation_error",
"message": "Request validation failed.",
"details": [
{
"field": "address.zip_code",
"code": "invalid_format",
"message": "Must be a 5-digit or 9-digit ZIP code."
},
{
"field": "items[0].quantity",
"code": "out_of_range",
"message": "Must be at least 1."
},
{
"field": "items[2].sku",
"code": "not_found",
"message": "SKU 'INVALID-123' does not exist."
}
],
"request_id": "req_def456"
}
}
The items[0].quantity notation tells the developer exactly which item in the array has the problem. Without array indices, the developer cannot identify which of potentially dozens of items failed validation.
Return All Errors, Not Just the First
Consider a registration endpoint that receives this request:
{
"email": "not-an-email",
"password": "123",
"name": "",
"age": -5
}
Bad — returning only the first error:
{
"error": {
"code": "invalid_format",
"field": "email",
"message": "Must be a valid email address."
}
}
The developer fixes the email, resubmits, and gets a new error about the password. Fixes the password, resubmits, gets an error about the name. Four round trips to discover four errors.
Good — returning all errors:
{
"error": {
"code": "validation_error",
"message": "Request validation failed.",
"details": [
{
"field": "email",
"code": "invalid_format",
"message": "Must be a valid email address."
},
{
"field": "password",
"code": "too_short",
"message": "Must be at least 8 characters."
},
{
"field": "name",
"code": "required",
"message": "Name is required."
},
{
"field": "age",
"code": "out_of_range",
"message": "Must be a positive number."
}
],
"request_id": "req_ghi789"
}
}
One round trip. All four problems identified. The developer fixes everything and resubmits successfully.
This matters at scale. An API that returns one error at a time during a bulk import of 10,000 records with a 5% error rate creates 500 round trips instead of one.
Standardized Validation Error Codes
Define a consistent set of validation error codes and use them across all endpoints:
required — field is missing or null
invalid_format — field does not match expected format
too_short — string is shorter than minimum length
too_long — string exceeds maximum length
out_of_range — number is outside allowed range
not_found — referenced resource does not exist
already_exists — value must be unique but already exists
invalid_type — field type does not match (string vs number)
pattern_mismatch — field does not match regex pattern
invalid_enum — value is not in the allowed set
Consumers rely on these codes for programmatic error handling:
if error.code == "already_exists" and error.field == "email":
show_login_suggestion()
elif error.code == "too_short" and error.field == "password":
show_password_requirements()
Changing these codes is a breaking change. Treat them as part of your API contract.
Client-Side vs Server-Side Validation
Always Validate on the Server
Client-side validation is a user experience optimization. Server-side validation is a security requirement. Never trust client-side validation alone.
Client sends: {"age": 25}
Client-side validation: passes (number between 1-150)
Client sends: {"age": -999}
Server-side validation: fails (out_of_range)
A malicious client can bypass any client-side validation by sending requests directly to the API. Your server must validate every field on every request, regardless of what the client does.
Client-Side Validation as Enhancement
Client-side validation provides instant feedback without a network round trip. It should mirror your server-side rules but never replace them.
Expose your validation rules so clients can implement matching validation:
{
"fields": {
"email": {
"type": "string",
"required": true,
"format": "email",
"max_length": 254
},
"password": {
"type": "string",
"required": true,
"min_length": 8,
"max_length": 128
},
"age": {
"type": "integer",
"required": false,
"min": 13,
"max": 150
}
}
}
This can be derived from your OpenAPI schema. Clients that implement matching validation reduce unnecessary API calls. But the server must still validate, because any client might send unvalidated data.
Input Sanitization
Trim Whitespace
Leading and trailing whitespace in string fields is almost always unintentional:
{
"email": " jane@example.com ",
"name": " Jane Doe "
}
Trim whitespace on the server side. Do not reject the input — fix it silently. The user meant jane@example.com, not jane@example.com .
Normalize Formats
For fields with known formats, normalize before validating:
Phone: "(555) 123-4567" → "+15551234567"
URL: "example.com" → "https://example.com"
Email: "Jane@Example.COM" → "jane@example.com"
Normalization reduces validation failures caused by format differences, not actual errors.
Reject Unexpected Fields
When a request includes fields that your endpoint does not accept, you have two options:
Strict mode — reject the request with an error:
{
"error": {
"code": "validation_error",
"message": "Request contains unexpected fields.",
"details": [
{
"field": "username",
"code": "unexpected_field",
"message": "This endpoint does not accept 'username'. Did you mean 'name'?"
}
]
}
}
Lenient mode — ignore unexpected fields silently.
Strict mode catches typos and prevents confusion. If a developer sends emial instead of email, strict mode tells them immediately. Lenient mode silently drops the field and the developer wonders why the email was not saved.
Stripe uses strict mode for most endpoints. A request with an unrecognized parameter returns an error immediately. This catches bugs early.
Contextual Validation
Some validation depends on the state of other fields or external data:
Cross-Field Validation
{
"start_date": "2025-03-01",
"end_date": "2025-02-01"
}
{
"error": {
"code": "validation_error",
"message": "Request validation failed.",
"details": [
{
"field": "end_date",
"code": "invalid_range",
"message": "end_date must be after start_date."
}
]
}
}
State-Dependent Validation
POST /v1/orders/ord_123/ship
{
"error": {
"code": "invalid_state",
"message": "Order cannot be shipped because it has not been paid.",
"details": [
{
"field": "status",
"code": "invalid_transition",
"message": "Order status is 'pending_payment'. Must be 'paid' before shipping."
}
]
}
}
Uniqueness Validation
{
"email": "jane@example.com"
}
{
"error": {
"code": "validation_error",
"message": "Request validation failed.",
"details": [
{
"field": "email",
"code": "already_exists",
"message": "An account with this email address already exists."
}
]
}
}
Be careful with uniqueness validation in authentication contexts. On a registration endpoint, telling a user that "this email already exists" confirms that the email has an account, which is an information disclosure. Balance usability with security.
HTTP Status Codes for Validation
Use the right status code for validation failures:
- 400 Bad Request — the request body is malformed JSON, missing required headers, or syntactically invalid
- 422 Unprocessable Entity — the request is syntactically valid but semantically incorrect (validation failures)
The distinction matters. A 400 means "I cannot parse your request." A 422 means "I parsed your request but the data is wrong."
Some APIs use 400 for both cases. This is acceptable if you are consistent. Using 422 for validation errors provides clearer semantics and helps with automated error categorization.
POST /v1/users
Content-Type: application/json
{invalid json here}
→ 400 Bad Request (cannot parse)
POST /v1/users
Content-Type: application/json
{"email": "not-valid", "name": ""}
→ 422 Unprocessable Entity (parsed but invalid)
Common Pitfalls
- Returning one error at a time — the single biggest validation UX failure. Always collect and return all validation errors in one response.
- Generic field names — using
field1orparaminstead of the actual field name. The error must reference the exact JSON path of the problematic field. - Missing error codes — returning only a human-readable message without a machine-readable code. Clients need codes for programmatic handling.
- Trusting client-side validation — skipping server-side validation because the client "already validates." Any HTTP client can bypass your frontend.
- Inconsistent codes across endpoints — using
invalid_emailon one endpoint andemail_invalidon another. Standardize your error codes. - Leaking information through validation — telling users "this email is already registered" on a login page reveals account existence to attackers.
- Not trimming input — rejecting
" jane@example.com "because of whitespace. Trim first, then validate.
Key Takeaways
- Return ALL validation errors at once, not one at a time. This is the single most impactful improvement you can make to validation UX.
- Use field-level error objects with three components:
field(JSON path),code(machine-readable), andmessage(human-readable). - Define a standard set of validation error codes (required, invalid_format, too_short, out_of_range) and use them consistently across all endpoints.
- Always validate on the server. Client-side validation is a UX enhancement, not a security measure.
- Trim whitespace and normalize formats before validating. Reject unexpected fields in strict mode to catch typos early.
- Use 422 for semantic validation errors and 400 for syntax errors. Be consistent across your entire API.