= Jackson Bean Validation Module :version: 0.6.0 :groupId: org.unbroken-dome.jackson-bean-validation :artifactId: jackson-bean-validation
image:https://img.shields.io/maven-central/v/{groupId}/{artifactId}[]
This is a https://github.com/FasterXML/jackson[Jackson] extension to perform https://beanvalidation.org/2.0/[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:
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):
boolean
)dateOfBirth
might be in a format that is not parseable to a LocalDate
.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:
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 https://search.maven.org/artifact/org.unbroken-dome.jackson-bean-validation/jackson-bean-validation/{version}/bundle[Maven Central].
Add the following dependency to your build script:
repositories { mavenCentral() }
In Java code, register an instance BeanValidationModule
with your ObjectMapper
. It also
requires a ValidatorFactory
instance for performing validation. (For example,
http://hibernate.org/validator/[Hibernate Validator] is a widespread implementation of Bean Validation).
ValidatorFactory validatorFactory = Validation.byDefaultProvider() .configure() .buildValidatorFactory();
BeanValidationModule module = new BeanValidationModule(validatorFactory);
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.
All property paths in the ConstraintViolation
objects refer to the property names in the input JSON, not
the Java bean property names. They might be different if you use @JsonNaming
with a custom name mapping strategy,
or @JsonProperty
with explicit names.
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:
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
or requiredMessage
on the @JsonValidated
annotation:
Global level: Put the messages in your ValidationMessages.properties
(or locale-specific variants):
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:
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:
So, you no longer need to use String?
just to validate @NotNull
and use those ugly double exclamation
marks everywhere.
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).
null
value for the property, it will always be considered a violation of
the NotNull
constraint.NotNull
by default,
but this behavior can be controlled with the BeanValidationFeature.VALIDATE_KOTLIN_LATEINIT_VARS
feature flag.
You may want to switch off this behavior if you intend to initialize the lateinit 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.