Expand Authors Top

If you have a few years of experience in the Java ecosystem and you’d like to share that with the community, have a look at our Contribution Guidelines.

Expanded Audience – Frontegg – Security (partner)
announcement - icon User management is very complex, when implemented properly. No surprise here.

Not having to roll all of that out manually, but instead integrating a mature, fully-fledged solution - yeah, that makes a lot of sense.
That's basically what Frontegg is - User Management for your application. It's focused on making your app scalable, secure and enjoyable for your users.
From signup to authentication, it supports simple scenarios all the way to complex and custom application logic.

Have a look:

>> Elegant User Management, Tailor-made for B2B SaaS

November Discount Launch 2022 – Top
We’re finally running a Black Friday launch. All Courses are 30% off until end-of-day today:

>> GET ACCESS NOW

November Discount Launch 2022 – TEMP TOP (NPI)
We’re finally running a Black Friday launch. All Courses are 30% off until end-of-day today:

>> GET ACCESS NOW

1. Overview

In this tutorial, we'll see how we can use Java's Validation API to validate objects after deserialization.

2. Trigger Validation Manually

Java's API for bean validation is defined in the JSR 380. A common use of it is @Valid annotated parameters in Spring controllers. However, in this article, we'll focus on validation outside of controllers.

First, let's write a method that will validate that the content of an object complies with its validations constraints. To do this, we'll get the Validator from the default validator factory. Then, we'll apply the validate() method to the object. This method returns a Set of ConstraintViolation. A ConstraintViolation encapsulates some hints about the validation error. To keep it simple, we'll just throw a ConstraintViolationException in case any validation problem occurs:

<T> void validate(T t) {
    Set<ConstraintViolation<T>> violations = validator.validate(t);
    if (!violations.isEmpty()) {
        throw new ConstraintViolationException(violations);
    }
}

If we call this method on an object, it will throw if the object doesn't respect any validation constraints. This method can be called at any point on an existing object with attached constraints.

3. Incorporate the Validation into the Deserialization Process

Our goal is now to incorporate the validation into the deserialization process. Concretely, we'll override Jackson‘s deserializer to perform the validation right after the deserialization. This will ensure that any time we deserialize an object, we don't allow any further treatment if it's not compliant.

First, we need to override the default BeanDeserializer. A BeanDeserializer is a class that can deserialize objects. We'll want to call the base deserialization method and then apply our validate() method to the created instance. Our BeanDeserializerWithValidation looks like this:

public class BeanDeserializerWithValidation extends BeanDeserializer {

    protected BeanDeserializerWithValidation(BeanDeserializerBase src) {
        super(src);
    }

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        Object instance = super.deserialize(p, ctxt);
        validate(instance);
        return instance;
    }

}

The next step is to implement our own BeanDeserializerModifier. This will allow us to alter the deserialization process with the behavior defined in BeanDeserializerWithValidation:

public class BeanDeserializerModifierWithValidation extends BeanDeserializerModifier {

    @Override
    public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        if (deserializer instanceof BeanDeserializer) {
            return new BeanDeserializerWithValidation((BeanDeserializer) deserializer);
        }

        return deserializer;
    }

}

Lastly, we need to create an ObjectMapper and register our BeanDeserializerModifier as a Module. A Module is a way to extend Jackson's default functionalities. Let's wrap it in a method:

ObjectMapper getObjectMapperWithValidation() {
    SimpleModule validationModule = new SimpleModule();
    validationModule.setDeserializerModifier(new BeanDeserializerModifierWithValidation());
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(validationModule);
    return mapper;
}

4. Example Usage: Read and Validate Object from File

We'll now showcase a small example of how to use our custom ObjectMapper. First, let's define a Student object. A Student has a name. The name length must be between 5 and 10 characters:

public class Student {

    @Size(min = 5, max = 10, message = "Student's name must be between 5 and 10 characters")
    private String name;

    public String getName() {
        return name;
    }

}

Let's now create a validStudent.json file that contains the JSON representation of a valid Student object:

{
  "name": "Daniel"
}

We'll read the content of this file in an InputStream. First, let's define the method that parses an InputStream to a Student object and validates it at the same time. For this, we want to use our ObjectMapper:

Student readStudent(InputStream inputStream) throws IOException {
    ObjectMapper mapper = getObjectMapperWithValidation();
    return mapper.readValue(inputStream, Student.class);
}

We can now write a test in which we'll:

  • start by reading the content of the file into an InputStream
  • convert the InputStream to a Student object
  • check that the content of the Student object is coherent with what's expected

This test looks like this:

@Test
void givenValidStudent_WhenReadStudent_ThenReturnStudent() throws IOException {
    InputStream inputStream = getClass().getClassLoader().getResourceAsStream(("validStudent.json");
    Student result = readStudent(inputStream);
    assertEquals("Daniel", result.getName());
}

Similarly, we can create an invalid.json file containing the JSON representation of a Student with a name with less than 5 characters:

{
  "name": "Max"
}

Now we need to adapt our test to check that a ConstraintViolationException is indeed thrown. Additionally, we can check that the error message is correct:

@Test
void givenStudentWithInvalidName_WhenReadStudent_ThenThrows() {
    InputStream inputStream = getClass().getClassLoader().getResourceAsStream("invalidStudent.json");
    ConstraintViolationException constraintViolationException = assertThrows(ConstraintViolationException.class, () -> readStudent(inputStream));
    assertEquals("name: Student's name must be between 5 and 10 characters", constraintViolationException.getMessage());
}

5. Conclusion

In this article, we've seen how to override Jackson's configuration to validate an object right after deserializing it. Thus, we can guarantee that it's impossible to work on an invalid object afterward.

As always, the associated code is available over on GitHub.

November Discount Launch 2022 – Bottom
We’re finally running a Black Friday launch. All Courses are 30% off until end-of-day today:

>> GET ACCESS NOW

Jackson footer banner
guest
0 Comments
Inline Feedbacks
View all comments