Is a common code practice in our microservices to have a lot of If’s blocks to validate if some values are null, or if the have the right size, the dates are correct, etc. then the developer needs to inform the user that something went wrong and 50% of our code are those validations.
Fortunately within the JavaEE/JakartaEE API’s there is Bean Validation that allows the developer to reduce all of that validation code in a declarative way using annotations and can be used along with MicroProfile to write better microservices that not only informs the users that something went wrong but also let them know how to fix it.
All the code used in this post can be found at this repository:
Integrating Bean Validation in our project
To integrate Bean Validation in our project is enough to add the dependency to all the JavaEE/JakartaEE APi’s or just the Bean Validation one.
dependencies { /*--- Full JavaEE dependency or just javax.validation dependency ---*/ /* providedCompile group: 'javax', name: 'javaee-api', version: '8.0' */ providedCompile group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final' providedCompile group: 'org.eclipse.microprofile', name: 'microprofile', version: '2.1' }
Example using Gradle
Then the developer can start using the Bean Validation annotations on any POJO, for example:
- @NotNull: Verifies that the value is not null.
- @AssertTrue: Verifies that the value is true.
- @Size: Verifies that the size of the value is between the min and max specified, can be used with Strings, Maps, Collections and Arrays.
- @Min: Verifies that the numeric value is equal or greater than the specified value.
- @Max: Verifies that the numeric value is equal or lower than the specified value..
- @Email: Verifies that the value is a valid email.
- @NotEmpty:Verifies that the value is not empu, applies to Strings, Collections, Maps and Arrays.
- @NotBlank:Verifies that the text is not whitespaces.
- @Positive / @PositiveOrZero: Verifies that the numeric value is positive including or not the zero.
- @Negative / @NegativeOrZero: Verifies that the numeric value is negative including or not the zero.
- @Past / @PastOrPresent: Verifies that the date is in the past including or not the present.
- @Future / @FutureOrPresent: Verifies that the date is in the future including or not the present.
And can be used like this:
public class Book { private Long id; @NotNull @Size(min = 6) private String name; @NotNull private String author; @Min(6) @Max(200) @NotNull private Integer pages; }
Example of annotated POJO with validations
Integrating Bean Validation with JAX-RS
When Bean Validation is integrated in the project and POJOS are annotated is time to tell to JAX-RS endpoints to make or not the validations using the annotation @Valid like this:
@POST public Book createBook(@Valid Book book) { //Do something like saving to DB and then return the book with a new ID book.setId(20L); return book; }
Example of JAX-RS endpoint annotated to validate the Book Object
When calling the service all the validation on the Book objects will be executed and an error will be returned if something went wrong or if everything is ok the service code will be executed.
Web Service executed successfully
Web Service with error (name not sent)
Better error management
As can be seen in the previous image, when there is an error the service code was not executed and the standard error response of the server is sent, but this is not very useful at all and can be a cause of more errors if the client expects that all the responses will be returned in JSON format.
To improve the error responses and let the user know what happened an interceptor of type ExceptionMapper<ConstraintViolationException> can be written to transform the standard response into JSON and add more useful information to it.
@Provider public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> { private String getPropertyName(Path path) { //The path has the form of com.package.class.property //Split the path by the dot (.) and take the last segment. String[] split = path.toString().split("\\."); return split[split.length - 1]; } @Override public Response toResponse(ConstraintViolationException exception) { Map<String, String> errors = exception.getConstraintViolations().stream() .collect(Collectors.toMap(v -> getPropertyName(v.getPropertyPath()), ConstraintViolation::getMessage)); return Response.status(Response.Status.BAD_REQUEST) .entity(errors).type(MediaType.APPLICATION_JSON) .build(); } }
Exception mapper to transform the error response.
Once the exception mapper is registered using the @Provider annotation and calling the service if there is an error a response like this will be returned:
WebService with a JSON error report with details
Query, Path and Header Params
Bean Validation is not limited to be used in request objects, also can be used on Query, Path and Header params like this:
@GET public Book getBook(@Valid @NotNull @QueryParam("id") Long id) { //Just build a dummy book to return Book book = new Book(); book.setId(id); book.setAuthor("Jorge Cajas"); book.setPages(100); return book; }
But calling this service, at the error response we can be note that the ExceptionMapper previously written is not useful at all because the parameter name was not resolved.
The id parameter name was not resolved
To fix this Bean Validation must be configured in a way it knows how to resolve the JAX-RS method’s parameter names.
The validation.xml is used to configure various aspects of Bean Validation and must be placed at resources/META-INF directory. On this file we will use the <parameter-name-provider> property with a reference to a class that will be responsible to resolve the JAX-RS parameter names.
<validation-config xmlns="http://jboss.org/xml/ns/javax/validation/configuration" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/configuration validation-configuration-1.1.xsd" version="1.1"> <parameter-name-provider>com.demo.validations.CustomParameterNameProvider</parameter-name-provider> </validation-config>
resources/META-INF/validation.xml
The class CustomParameterNameProvider will inspect the JAX-RS methods and from the annotations of Query, Path or Header params will complete the necessary information in order to resolve correctly the parameter names on the Exception Mapper previously written.
public class CustomParameterNameProvider implements ParameterNameProvider { @Override public List<String> getParameterNames(Constructor<?> constructor){ return lookupParameterNames(constructor.getParameterAnnotations()); } @Override public List<String> getParameterNames(Method method) { return lookupParameterNames(method.getParameterAnnotations()); } private List<String> lookupParameterNames(Annotation[][] annotations) { final List<String> names = new ArrayList<>(); if (annotations != null) { for (Annotation[] annotation : annotations) { String annotationValue = null; for (Annotation ann : annotation) { annotationValue = getAnnotationValue(ann); if (annotationValue != null) { break; } } // if no matching annotation, must be the request body if (annotationValue == null) { annotationValue = "requestBody"; } names.add(annotationValue); } } return names; } private static String getAnnotationValue(Annotation annotation) { if (annotation instanceof HeaderParam) { return ((HeaderParam) annotation).value(); } else if (annotation instanceof PathParam) { return ((PathParam) annotation).value(); } else if (annotation instanceof QueryParam) { return ((QueryParam) annotation).value(); } return null; } }
CustomParameterNameProvider.java
Once Bean Validation is configured, a call to the service with error will be like this:
Id query param name resolver correctly