Validation analysis paralysis

Adam Warski
SoftwareMill Tech Blog
8 min readFeb 11, 2021

--

We can’t trust the data that reaches our servers. We have to make sure that it’s properly formatted, within range, that it references entities that exist in the database, or that the data matches dynamically defined business rules. A wide range of responsibility!

That’s why we need data parsers & validators. Frameworks and libraries try to help us with that task by defining various extension points where we can plug in our validation rules. But where to draw the line: when to perform what kind of validations? How much parsing & validation should be done outside the service layer (e.g. in the transport layer), and how much within?

Validation appears twice. What’s the responsibility of each validation stage?

I’ve faced this dilemma many times. When writing applications, if the service implementations receive validated data, “how much” validated should it be? What should be the responsibility of the framework-provided validators, and when should validation happen as part of the business logic? Can I access the database in the validators in the transport (HTTP) layer?

Libraries face similar problems. When writing tapir, a library for defining HTTP endpoints in a programmer-friendly way, I had the same issues: what kind of validation should the library allow? One of tapir’s features is OpenAPI documentation generation, and there’s only a certain class of validations that you can define according to that standard, so maybe that’s a good starting point. Still: are these constraints sensible, or somehow arbitrary?

Service contract

Another formulation of the question is what should be part of the service contract — how specialized should the data types be, which appear in the signature of the service methods? Of course, we have full liberty here, we can be as strict or as permissive as we wish and as the project requires. Still, we might attempt to formulate some best practices.

It’s quite natural that we distinguish between the most basic data formats, such as strings, numbers and booleans. We get this distinction even when working with the otherwise untyped JSON format. Hence only by parsing JSON (into a primitively-typed model, consisting of strings, numbers, booleans, objects, and arrays), we already get some validation.

How far can we stretch it? For example, a service might require that it should only be called given an activated user. This requirement can be captured using types or through documentation. However, this is definitely too strong: the calling side would need to perform considerable logic (query the database & check the activation flag). Instead, the service should accept a “raw” user id, perform a lookup and validate if the user is active within the service. Once again, we’re looking for the golden mean!

Types of validations

Not all validations are equal. While we won’t be able to formulate absolute rules, which will allow making fully objective decisions as to where to put validation rules, we might come up with some hints, making this slightly less of an art, and a bit more good engineering practice.

The first thing to consider is whether we want to validate a single value or multiple values in a group. In other words, is this a single data point (a primitive type) that we are validating, like a number or a string? Or are these multiple values, when many fields need to be validated together (a composite type)?

A related characteristic is whether to perform the validation, we need to access the context of the whole request, that is if the validation depends on some other user-provided values. Alternatively, validation rules can be context-free; then we can perform the validation without knowing anything else above the value that’s being validated, without accessing the parent or sibling values.

Finally, an important factor is whether our validation is a pure function, without side-effects, or is it effectful? Checking that a number is positive is vastly different from checking that a user with the given id exists in the database.

Validating simple types

One possibility of a rule-of-thumb as to when to validate a value outside of a service is if it meets these criteria:

  • is a single data point
  • can be validated without context
  • parsing & validation is pure (no side effects)

It’s often beneficial to capture values that meet such validation rules as separate data types, even if they are only thin wrappers around primitive objects. That is, we’re then rather talking about parsing than validation: a function that returns either an error or the parsed value, instead of an opaque boolean.

Having an Email or Age data type (in some languages, we have value classes, that allow making these zero-cost abstractions) increases code readability and eliminates a whole class of simple mistakes. If a reader of the service contract sees such a type in the signature, they understand that the service accepts a validated value. If a primitive one is used, this is not at all clear.

If you are interested in how parsing compares to validation, take a look at the “Parse, don’t validate” article by Alexis King.

Additionally, such validations can often be described using a simple, declarative language, such as Java’s annotations or the JSON-based validator description defined in the JSON schema. Due to the simplicity of that language, such rules can often be quite easily propagated to the client-side and additionally enforced there (for better user experience).

Validating other data

In all other cases — when there are multiple data points to validate, when the validation process needs to access the request’s context, or when the validating function is effectful — it’s best to leave parsing & validation to the service layer, and perform it in the business logic.

The majority of validation will then happen within the service. But why not? Anything above the most basic parsing/validation cases becomes part of the business logic and is provided as a functionality to all clients.

That last bit is important: we might have several clients or transport layers using our service. If we require complex validation rules to be enforced beforehand, all clients must replicate that functionality. It seems a solution here would be to capture e.g. multiple values as a type, which can be only constructed for a valid group of values. That would be an extension of how we can capture simple validations as type constraints. Is that a good way to go?

It might, but in many cases — it’s not. Parsing a single value, without context, using a pure function, is a basic, easy to understand operation. As mentioned before, it’s also easy to capture using a simple declarative language. On the other hand, if we have either:

  • a group of values to validate
  • validations that require context
  • effectful validations

usually, such validations are very domain-specific, and the most natural place for them is being combined with the business logic. Doing otherwise would expose that business logic to the outside world, which defeats the whole purpose of having a service that is supposed to provide encapsulation.

In my early JavaEE days, I remember abusing JSR-303 custom validators by creating ones that looked up managed beans from the CDI context. This allowed me to run complex validation logic, and even access a database. However, the resulting code was neither readable, maintainable, nor testable.

Case study: validating IBAN numbers

As an example and a case study, let’s look at validating bank account numbers in the IBAN (International Bank Account Number) format. It consists of a two-letter country code and a varying number of digits, which contain a control sum, identifiers of the bank, and of the account within the bank.

The example is taken from a blog post by Michał Chmielarz, also on validation, using JSR-380 (Bean Validation 2.0) in Java. Michał also discusses the problem of validations leaking outside the domain services. Take a look!

Let’s look at a couple of validation scenarios, and where we might want to perform them:

  1. validating the basic structure of an IBAN: two letters + digits. That amounts to verifying a simple pattern, using a single data point, in a context-free and pure way. Values validated in that way could be captured as an InputIBAN value and used as part of the service’s contract. Any clients of the service will need to provide IBANs meeting these basic criteria. Hence this validation is performed at the transport or client layers.
  2. validating the country code and bank identifiers. This uses dictionaries to verify if the given country code is valid and if a bank with a given identifier exists. While country codes are rather static, bank identifiers are not. Hence, at least the latter part should be part of the business logic, as the validation might involve side-effects and is very domain-specific.
  3. validating the control sum. This is again country-specific, with various rules for various countries. We can’t expect the clients to perform this logic. Best kept within the service logic. Combining with the previous check, we might end up with a side-effecting function within the service logic, which turns InputIBAN into a fully-validated and parsed IBAN type.
  4. having two IBAN numbers, validating that they belong to the same country or that a transfer can be made between them. The first rule sounds simple, the second — not so much. However, as this concerns checking multiple values, according to our rules, this would be part of the service’s logic. These rules are difficult to capture as types, would require documentation and discipline in following the rules on the client-side. Hence it seems that the service is indeed where that logic should be.
  5. validating that an IBAN is in the same country, as the given user’s address. Here we’ve got a validation rule that needs to access the context of the request. Following similar reasoning as above, we decide to perform this validation in the service’s logic.

Summing up

Validation is not an easy subject, and deciding where to put which parts of the validation logic is a source of many heated discussions. While there are no bullet-proof rules, we can try to formulate “rules of thumb”.

In this process, it might be helpful to examine validation requirements, and classify them according to a couple of characteristics:

  • is it a single value, or multiple values that we are validating?
  • will validation need to access the context of the request?
  • is validation a side-effecting, or a pure operation?

It might be tempting to overuse the validation capabilities provided by frameworks and libraries. After all, since they are there — let’s use these features to perform as much validation as possible! Don’t — that’s a dead end. Chances are you’ll end up with services where the contract can be easily breached, or with code that’s not readable or testable.

The framework/library-provided validations form a “first line” of defense. They cover the most tedious and most common cases. You’ll end up with simpler, more readable and predictable code if you keep other validations as part of the business logic’s implementation. Such an approach is quite often “good enough”.

In other words, in the client or transport layer, we deal with the most obvious category of errors — enforcing simple formats, patterns, ranges, and enumerations. Anything that is more complex than that is delegated to validation within the service.

Keep in mind, that we were talking only about forming best practices, not rigid and unbreakable rules. Each project is different, and as with everything else: “it depends”. Above all, remember that we strive to formulate a practical approach, which allows us to focus on more important problems than validation, rather than create unnecessarily binding constraints.

--

--

Software engineer, Functional Programming and Scala enthusiast, SoftwareMill co-founder