Is there a path to do data validation in an elegant, standard and concise way? The way that doesn’t fall ito a sin of unreadability, the way that helps us to keep most of the data validation logic together and which has most of the code already done for us by developers of popular Java frameworks?
Yes, there is.
For us, developers of CUBA Platform, it is very important to let our users to follow the best practices. We believe that validation code should be:
- Reusable and following DRY principle;
- Expressed in a clear and natural way;
- Placed in the place where developers expects it to see;
- Able to check data from different data sources: user input, SOAP or REST calls etc.
- Aware about concurrency;
- Called implicitly by the application, without need to call the checks manually;
- Showing clear, localized messages to a user using concise designed dialogs;
- Following standards.
In this article I’ll be using an application based on CUBA Platform for all the examples. However, since CUBA is based on Spring and EclipseLink, most of this examples will work for any other Java framework that supports JPA and bean validation standard.
DB Constraints Validations
Perhaps, the most common and straightforward way of data validation uses DB-level constraints, such as required flag (‘not null’ fields), string length, unique indexes and so on. This way is very natural for enterprise applications, as this class of software is usually heavily data-centric. However, even here developers often do mistakes, defining constraints separately for each tier of an application. This problem is often caused by splitting responsibilities between developers.
Let's take an example most of you faced with, or even participated :). If a spec says that the passport field should have 10 digits in its number, most probably it will be checked everywhere: by DB architect in DDL, by backend developer in the corresponding Entity and REST services, finally, by UI developer right in client source-code. Later on this requirement changes and size of the field grows up to 15 digits. Tech Support changes the DB constraint, but for a user it means nothing as the client side check will not be passed anyway...
Everybody knows the way to avoid this problem, validations must be centralized! In CUBA this central point of such kind of validation is JPA annotations over entities. Based on this meta information, CUBA Studio generates right DDL scripts and applies corresponding validators on the client side.
If JPA annotations get changed, CUBA updates DDL scripts and generate migration scripts, so next time you deploy your project, new JPA-based limitations will be applied to your application’s UI and DB.
Despite simplicity and implementation that spans up to DB level, and so is completely bullet-proof, JPA annotations are limited by the simplest cases that can be expressed in DDL standard without involving DB-specific triggers or stored procedures. So, JPA-based constraints can ensure that entity field is unique or mandatory or can define maximum length for a varchar column. Also, you can define unique constraint to the combination of columns with @UniqueConstraint annotation. But this is pretty much it.
However, in the cases that require more complex validation logic like checking for maximum and minimum values of a field or validating with a expression or doing a custom check that is specific to you application we need to utilize the well known approach called “Bean Validation”.
All we know, that it is a good practice to follow standards, which normally have long lifecycle and are battle-proven on thousands of projects. Java Bean validation is an approach that is set in stone in JSR 380, 349 and 303 and their implementations: Hibernate Validator and Apache BVal.
Although this approach is familiar to many developers, it’s benefits are often underestimated. This is an easy way to add data validations even for legacy projects which allows you to express your validations in a clear, straightforward and reliable way as close to your business logic as it could be.
Using Bean Validation approach brings quite a lot benefits to your project:
- Validation logic is concentrated near your domain model: defining value, method, bean constraint is done in a natural way that allows to bring OOP approach to the next level.
- Bean Validation standard gives you tens of validation annotations out of the box, like: @NotNull, @Size, @Min, @Max, @Pattern, @Email, @Past, less standard like @URL, @Length, mighty @ScriptAssert and many others.
- You are not limited by predefined constraints and can define your own constraint annotations. You can make a new annotation also, by combining couple others or make a brand new one and define a Java class that will be served as a validator.
- For example, looking at our previous example we can define a class-level annotation @ValidPassportNumber to check that passport number follows right format which depends from the country field value.
- You can put constraints not just on fields and classes, but also on methods and method parameters. This is called “validation by contract” and is the topic of the later section.
CUBA Platform (as some other frameworks) calls these bean validations automatically when user submits the data, so user would get the error message instantly if validation fails and you don’t need to worry about running these bean validators manually.
Let’s take a look at the passport number example once again, but this time we’d like to add couple additional constraints on the entity:
- Person name should have length of 2 or more and be a well-formed name. Regexp is quite complex, but Charles Ogier de Batz de Castelmore Comte d'Artagnan passes the check and R2D2 does not :) ;
- Person height should be in interval: 0 < height <= 300 centimeters;
- Email string should be a properly formatted email address.
So, with all these checks the Person class looks like this:
I think that usage of standard annotations like @NotNull, @DecimalMin, @Length, @Pattern and others is quite clear and doesn’t need a lot of comments. Let’s see how custom @ValidPassportNumber annotation is implemented.
Our brand new @ValidPassportNumber checks that Person#passportNumber match the regexp pattern specific to each country defined by Person#country.
First, following the documentation (CUBA or Hibernate docs are good references), we need mark our entity class with this new annotation and pass groups parameter to it, where UiCrossFieldChecks.class says that the check should be called after checking all individual fields on the cross-field check stage and Default.class keeps the constraint in the default validation group.
The annotation definition looks like this:
@Target(ElementType.TYPE) defines that the target of this runtime annotation is a class and @Constraint(validatedBy = … ) states that the annotation implementation is in ValidPassportNumberValidator class that implements ConstraintValidator<...> interface and has the validation code in isValid(...) method, which code does the actual check in a quite straightforward way:
That’s it. With CUBA platform we don’t need to write a line of code more than that to get our custom validation working and giving messages to a user if he/she made a mistake. Nothing complex, do you agree?
Now, let’s check how all this stuff work. CUBA has some extra goodies: it not just shows error messages to a user but also highlight form fields that hasn’t passed single-field bean validations with nice red lines:
Isn’t this a neat thing? You got nice error UI feedback in the user's browser just after adding couple Java annotations to your domain model entities.
Concluding this section, let’s briefly list once again what pluses bean validation for entities has:
- It is clear and readable;
- It allows to define value constraints right in the domain classes;
- It is extendable and customizable;
- It is integrated with many popular ORMs and the checks are called automatically before changes are saved to a database;
- Some frameworks also runs bean validation automatically when user submits data in UI (but if not, it’s not hard to call Validator interface manually);
- Bean validation is a well-known standard, so there is a lot of documentation in the Internet about it.
But what shall we do if we need to set constraint onto a method, a constructor or some REST endpoint to validate data coming from external system? Or if we want to check the method parameters values in a declarative way without writing boring code full of if-elses in an each method we need to have such check?
The answer is simple: bean validation can be applied to methods as well!
Validation by Contract
Sometimes, we need to make another step and go beyond just application data model state validation. Many methods might benefit from automatic parameters and return values validation. This might be required not just when we need to check data coming to a REST or SOAP endpoint but also when we want to express preconditions and postconditions for method calls to be sure that input data have been checked before method body executes or that the return values are in the expected range, or we just want to declaratively express parameters boundaries for better readability.
With bean validation, constraints can be applied to the parameters and return values of a method or constructors of any Java type to check for their calls preconditions and postconditions. This approach has several advantages over traditional ways of checking the correctness of parameters and return values:
- The checks don’t need to be performed manually in the imperative way (e.g. by throwing IllegalArgumentException or similar). We rather specify constraints declaratively, so we have more readable and expressive code;
- Constraints are reusable, configurable and customizable: we don’t need to write validation code every time we need to do the checks. Less code - less bugs.
- If a class or method return value or method parameter is marked with @Validated annotation, that the constraints check would be done automatically by the framework on every method call.
- If an executable is marked with @Documented annotation then it’s pre- and postconditions would be included in the generated JavaDoc.
As the result with the ‘validation by contract’ approach we have clear code, less amount of it which it is easier to support and understand.
Let’s look how does it looks like for a REST controller interface in the CUBA app. PersonApiService interface allows to get a list of persons from the DB with getPersons() method and to add a new person to the DB using addNewPerson(...) call. And remember: bean validation is inheritable! In other words, if you annotate some class or field or method with a constraint, all descendants that extends or implements this class or interface would be affected by the same constraint check.
Does this code snippet look pretty clear and readable for you? (With the exception of @RequiredView(“_local”) annotation which is specific for CUBA platform and checks that returned Person object has all fields loaded from the PASSPORTNUMBER_PERSON table).
@Valid annotation specifies that every object in the collection returned by getPersons() method need to be validated against Person class constraints as well.
CUBA makes these methods available at the next endpoints:
Let’s open the Postman app and ensure that validation works as expected:
You might have noted that the above example doesn’t validate passport number. This is because it requires cross-parameter validation of the addNewPerson method since passportNumber validation regexp pattern depends from the country value. Such cross parameter checks are direct equivalent to class-level constraints for entities!
Cross parameter validation is supported by JSR 349 and 380, you can consult hibernate documentation for how to implement custom cross-parameter validators for class / interface methods.
Beyond Bean Validation
Nothing is perfect in the world, and bean validation has has some limitations as well:
- Sometime you just want to validate an complex object graph state before saving changes to the database. For example you might need to ensure that all items from an order made by a customer of your e-commerce system could be fit in one of the shipping boxes you have. This is quite heavy operation amd doing such check every time users add new items to their orders isn’t the best idea. Hence, such check might need to be called just once before the Order object and it’s OrderItem objects are saved to the database.
- Some checks have to be made inside the transaction. For example, e-commerce system should check if there are enough items in stock to fulfill the order before committing it to the database. Such check could be done only from inside the transaction, because the system is concurrent and quantities in stock could be changed at any time.
Entity listeners in CUBA are quite similar to PreInsertEvent, PreUpdateEvent and PredDeleteEvent listeners that JPA offers to a developer. Both mechanisms allow to check entity objects before or after they get persisted to a database.
It’s not hard to define and wire up an entity listener in CUBA, we need to do two things:
- Create a managed bean that implements one of the entity listener interfaces. For validation purposes 3 of these interfaces are important:
- Annotate the entity object that plan to track with @Listeners annotation.
In comparison with JPA standard (JSR 338, chapter 3.5) CUBA platform’s listener interfaces are typed, so you don’t need to cast Object argument to start working with the entity. CUBA platform adds possibility of entities associated with the current one or calling EntityManager to load and change any other entities. All such changes would invoke appropriate entity listener calls as well.
Also CUBA platform supports soft deletion, a feature when entities in DB are just marked as deleted without deleting their records from the DB. So, for soft deletion CUBA platform would call BeforeDeleteEntityListener / AfterDeleteEntityListener listeners while standard implementations would call PreUpdate / PostUpdate listeners.
Let’s look at the example. Event listener bean connects to an Entity class with just one line of code: annotation @Listeners that accepts a name of the entity listener class:
And entity listener implementation may look like this:
Entity listeners are great choice when you:
- Need to make data check inside transaction before the entity object get persisted to a DB;
- Need to check data in the DB during the validation process, for example check that we have enough goods in stock to accept the order;
- Need to traverse not just given entity object, like Order, but visit the object that are in the association or composition with the entity, like OrderItems objects for the Order entity;
- Want to track insert / update / delete operations for just some of your entity classes, for example you want to track such events only for Order and OrderItem entities, and don’t need to validate changes in other entity classes during transaction.
CUBA transaction listener’s works in transactional context as well, but in comparison with entity listeners they get called for every database transaction.
This gives them the ultimate power:
- nothing can pass their attention, but the same gives them weaknesses:
- they are harder to write,
- they can downgrade performance significantly if performing too much unneeded checks,
- They need to be written much more careful: a bug in transaction listener might even prevent application from bootstrapping;
So, transaction listeners is a good solution when you need to inspect many different type of entities with the same algorithm, like feeding data to a custom fraud detector that serves all your business objects.
Let’s look at the example that checks if an entity is annotated with @FraudDetectionFlag annotation and if yes, runs the fraud detector to validate it. Once again, please note, that this method is called before every DB transaction gets committed in the system, so the code has to try to check as least objects as possible as fast as it can.
To become a transaction listener, managed bean should just implement BeforeCommitTransactionListener interface and implement beforeCommit method. Transaction listeners are wired up automatically when the application starts. CUBA registers all classes that implements BeforeCommitTransactionListener or AfterCompleteTransactionListener as transaction listeners.
Bean validation (JPA 303, 349 and 980) is an approach that could serve as a concrete foundation for 95% of the data validation cases that happen in an enterprise project. The big advantage of such approach is that most of your validation logic is concentrated right in your domain model classes. So it is easy to be found, easy to be read and be supported. Spring, CUBA and many libraries are aware about these standards and calls the validation checks automatically during UI input, validated method calls or ORM persistence process, so validation works like a charm from developer’s perspective.
Some software engineers see validation that impacts an applications domain models as being somewhat invasive and complex, they say that making data checks at UI level is a good enough strategy. However, I believe that having multiple validation points in UI controls and controllers is quite problematic approach. In addition, validation methods we have discussed here are not perceived as invasive when they integrate with a framework that is aware about bean validators, listeners and integrates them to the client level automatically.
At the end, let’s formulate a rule of thumb to choose the best validation method:
- JPA validation has limited functionality, but is a great choice for simplest constraints on entity classes if such constraints can be mapped to DDL.
- Bean Validation is flexible, concise, declarative, reusable and readable way to cover most of the checks that your could have in your domain model classes. This is the best choice in most cases once you don’t need to run validations inside a transaction.
- Validation by Contract is a bean validation but for method calls. Use it when you need to check input and output parameters of a method, for example in a REST call handler.
- Entity listeners: although they are not such declarative as Bean Validation annotations, they are great place to check big object’s graphs or make a check that needs to be done inside a database transaction. For example, when you need to read some data from the DB to make a decision. Hibernate has analogs of such listeners.
- Transaction listeners are dangerous but ultimate weapon that works inside transactional context. Use it when you need to decide at runtime what objects have to be validated or when you need to check many different types of your entities against the same validation algorithm.
I hope that this article refreshed your memories about different validation methods available in Java enterprise applications and gave you couple ideas how to improve architecture of the projects you are working on.