Structuring validation errors in REST APIs
JSON REST API over HTTP is probably the most common method for integrating mobile, web, TV, (and more) applications with backend systems. It is also widely used for server-to-server communication. Yet, when I was looking for some articles about structuring validation errors in REST APIs I haven’t found much. And most of the described solutions were not satisfying.
As a result, I decided to design format that will fulfil my needs. Requirements emerged when I was working on two completely different projects. Thanks to that, they should be generic enough to fit into the majority of use cases. The requirements that I gathered were following:
- It should allow to aggregate multiple errors that occurred. Instead of returning first one.
- It should allow a client to link validation errors with invalid fields in complex data structures. Like nested lists and dictionaries.
- It should give client full control over presentation. By returning errors in machine-readable format.
- It should be JSON serializable
Basic example
Let’s start with a simple example. Assume that our API expects request body in JSON format. It should be a dictionary with two fields: username
and password
. In API we want to validate if request body doesn’t have empty name
or password
. When a client sends such request body:
{"username": "", "password": ""}
Our API should return validation errors formatted as follows:
[
{"error": "isBlank", "path": ["username"]},
{"error": "isBlank", "path": ["password"]}
]
As you can see the generic structure is a list of objects, where every element describes the single error. It allows to aggregate multiple validation errors. Each object has two mandatory fields. error
which contains a name of an error in machine readable format. And path
which references field where an error occurred.
API gives client full control over presentation, because error
has machine readable format. It doesn’t impose already translated message or message format. It also gives client all necessary data to tell user what happened (error
) and where (path
)
Complex data structures validation
One purpose of this format is to allow to aggregate multiple validation errors from complex data structures. path
property allows to reference any field. No matter how deeply it lives inside data structure. It contains all object properties and array indexes that leads to given item.
For example. If we want to reference name
field inside second array element in the following JSON:
{
"users": [
{"name": "Foo"},
{"name": "Bar"}
]
}
We need to use following path: ["users", 1, "name"]
. First element references users
field in top-level object. Next is index of array item (zero-indexed) 1
. Finally we have name
field.
Let’s see how it works with validation errors. Assume that we send following JSON to API in request body:
{"users": [
{"username": ""},
{"username": "Foo Bar"},
{"username": ""}
]}
API requires that username
field can not be blank. In that case we should get following validation errors:
[
{"error": "isBlank", "path": ["users", 0, "username"]},
{"error": "isBlank", "path": ["users", 2, "username"]}
]
What if we want to put constraint on array, such as maximum number of items? Then path should reference array itself instead of array specific elements. For example
[
{"error": "tooManyElements", "path": ["users"], "maxElements": 10}
]
Multiple errors per field
It isn’t rare case when single field has multiple constraints attached. For example, assume that username
field can’t be longer than 32 characters and must not contain any vulgarisms. If request sent by client breaches both constraints API could return multiple errors for the same path:
[
{"error": "tooLong", "path": ["username"], "maxLength": 32},
{"error": "hasVulgarisms", "path": ["username"]}
]
Error details
This format is easily extensible if we want to provide more details for given error. Thanks that client application has more data to produce more detailed error message. For example, assume that our API expects that password is at least 6 characters long. When client sends too short password, API can return following error:
[
{"error": "tooShort", "path": ["password"], "minLength": 6}
}
Thanks that client can render error message like “Password must be at least 6 characters long”. It’s crucial that every error type should always have the same set of details fields. For example, tooShort
error should always be accompanied by minLength
field with no exceptions. Without such constraint, API responses will be much harder to handle by clients.
Missing and redundant fields, non-JSON request body
There are few special cases that may require additional explanation.
Users rarely communicate directly with API. Instead they are using client applications. So, it’s client application responsibility to send exactly such data structure that API is expecting. Otherwise, even if user will receive error message about missing / redundant fields he won’t be able to do anything with it. However, detailed validation errors could still be pretty helpful for client application developers. They’ll see which fields are missing or redundant.
Let’s see an example. Assume that API requires single username
field in the request body. However, a client sends following request:
{"age": 24}
In that case, API should return following error:
[
{"error": "isRequired", "path": ["username"]},
{"error": "isRedundant", "path": ["age"]}
]
Last special case is when our API can not decode request that it received (e.g. because it is not valid JSON). This case deserves separate error type. It should be distinguished from validation errors described above. If you are using “Problem Details” standard described in RFC 7807 then you can use different type
or not parsable JSON request:
{
"type": "/json-parse-error",
"title": "Unable to parse request body as JSON"
}
While, for validation errors you could use following envelope
{
"type": "/validation-error",
"title": "Invalid request",
"validationErrors": [
// list of validation errors
]
}
Request body, URL and query parameters validation
So far I focused on HTTP request body validation. But nothing stands in the way to use it also for validation of other parts of the request. Such as path or query parameters. To achieve that we can prepend path of every validation error with the name of part of the request.
In most cases, we’ll validate request URL, body and query parameters. So, we can distinguish following prefixes: $query
, $body
and $url
.
URL part we’ll usually treat as a list. So path
parameter will contain an index of invalid URL segment.
For example, let’s assume that we have following URL template: /question/{id}/{userId}
. It expects that id
and userId
are integers. It also allows for extra query parameter: direction
. And it can have one of two values: ascending
or descending
For invalid request like /question/10/def?direction=foo
API should return following validation errors:
[
{"error": "notInteger", "path": ["$url", 1]},
{"error": "invalidValue", "path": ["$query", "direction"]}
]
As you can second invalid URL segment (userId
) is referenced by path: ["$url", 1]
. And invalid query parameter is referenced by ["$query", "direction"]
.
Summary
I hope that you find this solution simple and elegant. But, there is one more advantage of this format. You should be able to easily implement it by taking advantage of existing validation libraries. So far, I’ve done it in two distinct projects. First was written in Python, second in PHP. In next article, I would like to describe how to move this concept into actual implementation.