Skip to content

A Jackson module that performs Java Bean Validation during deserialization.

License

Notifications You must be signed in to change notification settings

unbroken-dome/jackson-bean-validation

Repository files navigation

Jackson Bean Validation Module

jackson bean validation

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 a LocalDate.

  • 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:

  1. Deserialize the JSON payload into the Java object.

  2. 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:

Gradle (build.gradle)
repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.unbroken-dome.jackson-bean-validation:jackson-bean-validation:0.6.0'
}
Maven (pom.xml)
<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 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.

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 or requiredMessage 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.properties
    org.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 the NotNull 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 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.