Jackson Bean Validation Module
This is a Jackson extension to perform Java Bean Validation during deserialization.
The Problem
This module is aimed mostly at REST APIs that should validate incoming JSON request bodies, but it may be useful in other scenarios.
Imagine the following JSON request body:
{
"firstName": "John",
"lastName": "Doe",
"dateOfBirth": "1983-01-25"
}
In application code, This might be mapped to the following Java class with Jackson and validation constraints:
public class PersonRequest {
@NotEmpty
String firstName;
@NotEmpty
String lastName;
@Past
LocalDate dateOfBirth; // requires JavaTimeModule
// Getters and setters omitted
}
This works in the success case, but there could be a number of things that are wrong about the request (assuming that it is syntactically correct JSON):
-
Any of the properties might be missing.
-
Any of the properties might have a wrong type in JSON (e.g. a
boolean
) -
The
dateOfBirth
might be in a format that is not parseable to aLocalDate
. -
Any of the validation constraints might be violated.
All of these cases are typically handled by sending a 400 Bad Request
response back to the client, ideally with a description of all that was wrong with the request. This is especially important if such errors should be displayed on a form in a UI.
In application code, this is most often handled with a two-step process:
-
Deserialize the JSON payload into the Java object.
-
Run the Bean
Validator
on it to find any constraint violations.
The big problem is that if step 1 fails (for example because dateOfBirth
was not parseable to a LocalDate
), Jackson will throw an exception, and the response will only contain that single error information. Also, from the Jackson exception it can be hard to pinpoint the exact property in the request that had a bad value. The client will have to fix that one error and try again, only to find out that there are more errors that had not been reported earlier.
For example, with the following request:
{
"firstName": "",
"dateOfBirth": "01-25"
}
This should ideally return a 400 Bad Request
response informing about 3 violations (firstName
must not be empty, lastName
is missing, and dateOfBirth
has a wrong format). However, since Jackson throws a MismatchedInputException
on parsing the dateOfBirth
, the client won’t even get to see the firstName
and lastName
violations until they try again with a correct dateOfBirth
.
Obviously, the only solution to this is to merge steps 1 and 2 into one, and perform the validation immediately during deserialization. This is where this module comes in.
Usage
The module library is available on Maven Central.
Add the following dependency to your build script:
repositories {
mavenCentral()
}
dependencies {
implementation 'org.unbroken-dome.jackson-bean-validation:jackson-bean-validation:0.6.0'
}
<dependency>
<groupId>org.unbroken-dome.jackson-bean-validation</groupId>
<artifactId>jackson-bean-validation</artifactId>
<version>0.6.0</version>
</dependency>
In Java code, register an instance BeanValidationModule
with your ObjectMapper
. It also requires a ValidatorFactory
instance for performing validation. (For example, Hibernate Validator is a widespread implementation of Bean Validation).
ValidatorFactory validatorFactory = Validation.byDefaultProvider()
.configure()
.buildValidatorFactory();
BeanValidationModule module = new BeanValidationModule(validatorFactory);
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(module);
Annotate all classes that should be validated with @JsonValidated
. If this annotation is not present, no validation will be performed.
@JsonValidated
public class PersonRequest {
@NotEmpty
String firstName;
@NotEmpty
String lastName;
@Past
LocalDate dateOfBirth;
// Getters and setters omitted
}
To cascade the bean validation to nested beans, you can also annotate the property or constructor parameter with @Valid
.
@JsonValidated
public class PersonRequest {
static class Name {
@NotEmpty String firstName;
@NotEmpty String lastName;
}
@Valid Name name;
}
Handling Violations
Deserialization of this object, with the BeanValidationModule
activated, might now throw a ConstraintViolationException
that contains all the violations of the input document, including JSON deserialization issues as well as constraint violations.
Note
|
Property Paths
All property paths in the The reason for this is that we’re conceptually validating the JSON object and not the Java bean (which is just being constructed). |
To deal with errors that would otherwise result in exceptions thrown by Jackson, the module introduces two "pseudo" constraints that are used for reporting these as constraint violations (even if they are not placed on the properties).
JsonValidInput
The module introduces a pseudo-constraint JsonValidInput
that will be reported as violated whenever Jackson would otherwise throw a MismatchedInputException
.
In the above examples, a value for dateOfBirth
that cannot be parsed to a LocalDate
would be reported as a violation of the JsonValidInput
constraint, including the path to that property.
You can also place @JsonValidValue
directly on a property in case you want a customized validation message:
@JsonValidValue(message = "Please enter a date in the format YYYY-MM-DD")
@Past
LocalDate dateOfBirth;
Note that @JsonValidValue
is not an actual constraint annotation (it is not meta-annotated with @Constraint
); placing it on a property is only for customization of the constraint parameters.
JsonRequired
The second pseudo-constraint is JsonRequired
; it is violated if there are any missing properties that are marked as required using the @JsonProperty
annotation:
public class PersonRequest {
@JsonCreator
public PersonRequest(
@JsonProperty(value="firstName", required=true) String firstName
@JsonProperty(value="lastName", required=true) String lastName,
@JsonProperty(value="dateOfBirth") LocalDate dateOfBirth) {
// ...
}
}
In this example, if firstName
and/or lastName
are missing in the input, they would be reported as a violation to JsonRequired
.
Note
|
JsonRequired violations are not triggered if the value is present in the JSON input but explicitly set to null . Use the standard @NotNull constraint to catch this case. |
Again, you could place @JsonRequired
directly on a property; this has the same effect as @JsonProperty(required = true)
but also allows you to customize the validation message.
Customizing Validation Messages
For JsonValidInput
and JsonRequired
, there are three ways to provide validation messages (in order of precedence):
-
Property level: Put the annotation directly on the validated property, and set its
message
argument (as described above). -
Class level: Set the
validInputMessage
orrequiredMessage
on the@JsonValidated
annotation:@JsonValidated( validInputMessage="is not valid", requiredMessage="is required") public class PersonRequest { // ... }
-
Global level: Put the messages in your
ValidationMessages.properties
(or locale-specific variants):ValidationMessages.propertiesorg.unbrokendome.jackson.beanvalidation.JsonValidInput.message=is not valid org.unbrokendome.jackson.beanvalidation.JsonRequired.message=is required
Note that the global messages should always be configured; the module library cannot provide defaults because there cannot be a second
ValidationMessages.properties
on the classpath.
Cross-Parameter Validation with @AssertTrue
@AssertTrue
constraints on instance methods are a common pattern with Bean Validation to perform cross-parameter validation. With the bean validation module, this may not work as intended because the properties are validated independently as they are deserialized, and the bean will not even be constructed if any of the property values violates the constraints.
To enable evaluation of an @AssertTrue
constraint, enable the BeanValidationFeature.VALIDATE_BEAN_AFTER_CONSTRUCTION
feature flag, which will cause the bean to be validated as a whole after it is fully constructed. Even so, such a violation will only be reported if the bean can be constructed, so a violation may not be visible if there are other violations on creator properties (i.e. constructor params).
Kotlin Support
The module should work well with Kotlin, and together with the KotlinModule
from jackson-module-kotlin
. I would recommend to always use data
classes where all properties are initialized in the constructor.
It is especially useful to perform NotNull
checks on constructor arguments that are not nullable in Kotlin, because the validation happens before the constructor is called:
@JsonValidated
data class PersonRequest(
@param:NotNull val firstName: String,
@param:NotNull val lastName: String,
@param:Past val dateOfBirth: LocalDate)
The validating deserializer will automatically detect nullability of constructor parameter types, and treat the parameters with non-nullable types as if they had an implicit @NotNull
annotation. So the following is equivalent to the example above:
@JsonValidated
data class PersonRequest(
val firstName: String,
val lastName: String,
@param:Past val dateOfBirth: LocalDate)
So, you no longer need to use String?
just to validate @NotNull
and use those ugly double exclamation marks everywhere.
Remember that annotations on val
parameters in the constructor should be qualified with @param:
. You can place multiple constraints with the shorthand syntax e.g. @param:[NotNull Size(min = 3)]
.
Handling of Required Parameters and Primitives
The standard KotlinModule
automatically treats all constructor parameters as required if they are not marked as nullable (e.g. String
instead of String?
). If such parameters are missing in the JSON input, a violation of JsonRequired
would be raised.
However, for primitive types this behavior only applies if the deserialization feature FAIL_ON_NULL_FOR_PRIMITIVES
is enabled (it is disabled by default). Otherwise, null
or missing values are mapped to the default value of the type (e.g. 0
for integers) even if the type is not nullable.
I would recommend enabling FAIL_ON_NULL_FOR_PRIMITIVES
when using Kotlin together with this module.
Late-init Properties
Kotlin’s lateinit var
properties are deserialized like other properties, and their values will be validated based on the annotations on the property. In addition, lateinit var
properties are treated as if they had an implicit NotNull
constraint, because they cannot have nullable or primitive types. An explicit @NotNull
annotation will still be honored if present (for example, to customize the validation message).
-
If the input JSON contains an explicit
null
value for the property, it will always be considered a violation of theNotNull
constraint. -
If the input JSON does not contain the property at all, it will be considered a violation of
NotNull
by default, but this behavior can be controlled with theBeanValidationFeature.VALIDATE_KOTLIN_LATEINIT_VARS
feature flag. You may want to switch off this behavior if you intend to initialize thelateinit var
properties programmatically after deserialization.
Jackson Version Compatibility
The module requires Jackson 2.9.x or higher. It does not work with Jackson 2.8.x.
Automated compatibility tests are run for the following Jackson versions:
Jackson major/minor | Tested compatibility |
---|---|
2.9 |
2.9.0 — 2.9.10 |
2.10 |
2.10.0 — 2.10.5 |
2.11 |
2.11.0 — 2.11.4 |
2.12 |
2.12.0 — 2.12.3 |
Limitations and Considerations
-
Jackson handles a plethora of corner-cases and custom annotations, and probably many of them are not working properly. The module should work for the most common cases (vanilla beans or constructor properties). If you spot an error with one of the more obscure Jackson features, please consider filing an issue.
-
Jackson views are currently not supported (they might just work, but lacking more extensive testing).
-
Validation groups are currently not supported - mostly because there is no nice way of passing them to the
ObjectMapper
when deserializing. -
Bean validation does not allow parameter validation on static methods. That means that static
@JsonCreator
factory methods will only be checked for valid input and required parameters, but actual bean validation constraints on these parameters will not be evaluated.