Structuring validation errors in REST APIs

Łukasz Lalik
5 min readJul 12, 2017

--

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.

--

--