In most applications, validation is typically done using a validation framework that helps setup validation faster. This article argues that using these validation frameworks is an antipattern to clean code. It introduces complexities that makes the code harder to debug, is hard/impossible to properly test and is not extendable enough. While validation frameworks offer quick setup and standardization, they can introduce hidden complexity and become limiting over time. Instead it is suggested that each project should do its validation manually, because it in the end is more maintainable and stable than the common approach.

Validation frameworks

Most validation frameworks are based around annotations/attributes where the class is annotated with the validation rules. For example:

class User {
	@NotNull
	String name;
	@NotNull @Email
	String email;
}

In this case the class User has two fields name and email both are not null and email is an email. This validation seems simple enough and is easy to write. In the development phase this is quickly breezed through and forgotten about. This is where the problems come in:

  1. Debugging. Rejections are harder to track with the annotation as it is hard to attribute a certain failing validation to an annotation. Especially with more advanced annotation knowing exactly why it failed is hard. An example could be a certain e-mail not parsing validation even though it is an email.
  2. Testing. These kind of validations are usually harder to test because they are not always triggered. Usually a validation service will have to be called that ends up turning any form of testing into an integration test.
  3. Extending. This problem breaks down further
    • Dependence between fields. Adding a dependence between two different fields are usually hard to do, or is hard to understand when done.
    • More advanced checks. More advanced checks usually ends up being written in a custom function that makes the validation a mix of annotations and custom functions. This is usually harder to reason about.
    • Error customization. If the errors returned has to be customized either because it is served differently to different clients or because it needs to be enriched.

Intentional validation

If you instead wanted to do it in an intentional way the class rules could for example be enforced by a static constructor:

class User {
	String name;
	String email;

	public static User of(String name, String email) {
		customNullCheck(name, email);
		if (!isEmail(email)) {
			throw new ValidationException(...);
		}
		return new User(name, email);
	}
}

From the debugging perspective it is already way easier to see what is actually going on in creation. Any exception will show the exact line of code that failed. It is trivial to add more if-statements if you wanted to extend the validation. Additionally a custom ValidationException could be used to customize errors are returned to the client. Writing tests are also easier to do as you just call the of method with pure Java code without any frameworks. Branching coverage will also expose these lines not being tested.

Languages like Rust provide an alternative in Result where the error is able to be returned as part of the result. This is incredibly powerful and is highly recommended over using exceptions.

Conclusion

Manual validation allows developers to write checks that are explicit, debuggable, testable, and extensible. While validation frameworks offer a quick setup, they often trade long-term maintainability for short-term convenience. By validating data intentionally—close to where it’s used—we can reduce complexity and improve code clarity. Choose manual validation when you value transparency and control.