unbroken-dome / jackson-bean-validation

A Jackson module that performs Java Bean Validation during deserialization.
MIT License
21 stars 13 forks source link
bean-validation jackson json json-validation validation

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

[source,json]

{ "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:

[source,java]

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

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:

[source,json]

{ "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 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:

[source,groovy,subs="+attributes"] .Gradle (build.gradle)

repositories { mavenCentral() }

dependencies { implementation 'org.unbroken-dome.jackson-bean-validation:jackson-bean-validation:{version}' }

[source,xml,subs="+attributes"] .Maven (pom.xml)

org.unbroken-dome.jackson-bean-validation jackson-bean-validation {version}

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).

[source,java]

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.

[source,java]

@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.

[source,java]

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

[source,java]

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

[source,java]

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

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

[source,kotlin]

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

[source,kotlin]

@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.

[INFO]

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).

== 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