@Valid When Creating Objects with Jackson Without Controller

@Valid when creating objects with jackson without controller

You can extend BeanDeserializer and validate object after deserialisation. To register this bean use SimpleModule.

Simple bean deserialiser with validation:

class BeanValidationDeserializer extends BeanDeserializer {

private final static ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
private final Validator validator = factory.getValidator();

public BeanValidationDeserializer(BeanDeserializerBase src) {
super(src);
}

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

return instance;
}

private void validate(Object instance) {
Set<ConstraintViolation<Object>> violations = validator.validate(instance);
if (violations.size() > 0) {
StringBuilder msg = new StringBuilder();
msg.append("JSON object is not valid. Reasons (").append(violations.size()).append("): ");
for (ConstraintViolation<Object> violation : violations) {
msg.append(violation.getMessage()).append(", ");
}
throw new ConstraintViolationException(msg.toString(), violations);
}
}
}

We can use it as below:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.module.SimpleModule;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.io.File;
import java.io.IOException;
import java.util.Set;

public class JsonApp {

public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();

SimpleModule validationModule = new SimpleModule();
validationModule.setDeserializerModifier(new BeanDeserializerModifier() {
@Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (deserializer instanceof BeanDeserializer) {
return new BeanValidationDeserializer((BeanDeserializer) deserializer);
}

return deserializer;
}
});

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(validationModule);

System.out.println(mapper.readValue(jsonFile, Pojo.class));
}
}

class Pojo {

@NotNull
@Size(min = 1, message = "Name should be at least 1 character.")
private String name;

@NotNull
@Pattern(regexp = "^https://github.com/.+/.+$", message = "Link to github should match https://github.com/USER/REPOSITORY")
private String github;

// getters, setters, toString()
}

For valid JSON payload:

{
"name": "Jackson",
"github": "https://github.com/FasterXML/jackson-databind"
}

prints:

Pojo{name='Jackson', github='https://github.com/FasterXML/jackson-databind'}

For invalid JSON payload:

{
"name": "",
"github": "https://git-hub.com/FasterXML/jackson-databind"
}

prints:

Exception in thread "main" javax.validation.ConstraintViolationException: JSON object is not valid. Reasons (2): Name should be at least 1 character., Link to github should match https://github.com/USER/REPOSITORY, 
at BeanValidationDeserializer.validate(JsonApp.java:110)
at BeanValidationDeserializer.deserialize(JsonApp.java:97)

See also:

  • Java Bean Validation Basics
  • Deserialize to String or Object using Jackson
  • Jackson custom serialization and deserialization

Java 8 Jackson validation

You used Spring Validator approach.
There is another approach:

J2EE JSR-303/JSR-349 Bean Validation API. it provides validation annotations (javax, not jackson).

See good example of both here

Java Spring web - convert sub-property to object with validator

If JSON payload does not fit Java model you need to implement custom deserialiser or Converter interface. Take a look at this example:

  • Jackson deserialize JSON object field into single list property

How do I enable strict validation of JSON / Jackson @RequestBody in Spring Boot REST API?

Behind the scene, Spring uses the Jackson library to serialize/deserialize POJO to JSON and vice versa. By default, the ObjectMapper that the framework uses to perform this task has its FAIL_ON_UNKNOWN_PROPERTIES set to false.

You can turn this feature on GLOBALLY by setting the following config value in application.properties.

spring.jackson.deserialization.fail-on-unknown-properties=true

Or if using YAML format, add the following to your application.yaml (or .yml) file):

spring:
jackson:
deserialization:
fail-on-unknown-properties: true

Subsequently, if you want to ignore unknown properties for specific POJO, you can use the annotation @JsonIgnoreProperties(ignoreUnknown=true) in that POJO class.


Still, this could mean a lot of manual work going forward. Technically, ignoring those unexpected data doesn't violate any software development principles. There might be scenarios where there's a filter or servlet sitting in front of your @Controller doing additional stuff that you're not aware of which requires those extra data. Does it seem worth the effort?

Spring Boot Validate JSON Mapped via ObjectMapper GET @RequestParam

In my point of view, Hibernate Bean Validator is probably one of the most convenient methods to validate the annotated fields of a bean anytime and anywhere. It's like setup and forget

  • Setup the Hibernate Bean Validator
  • Configure how the validation should be done
  • Trigger the validator on a bean anywhere

I followed the instructions in the documentation given here

Setup dependencies

I use Gradle so, I am going to add the required dependencies as shown below

// Hibernate Bean validator
compile('org.hibernate:hibernate-validator:5.2.4.Final')

Create a generic bean valdiator

I setup a bean validator interface as described in the documentation and then use this to validate everything that is annotated

public interface CustomBeanValidator {
/**
* Validate all annotated fields of a DTO object and collect all the validation and then throw them all at once.
*
* @param object
*/
public <T> void validateFields(T object);
}

Implement the above interface as follow

@Component
public class CustomBeanValidatorImpl implements CustomBeanValidator {
ValidatorFactory valdiatorFactory = null;

public CustomBeanValidatorImpl() {
valdiatorFactory = Validation.buildDefaultValidatorFactory();
}

@Override
public <T> void validateFields(T object) throws ValidationsFatalException {
Validator validator = valdiatorFactory.getValidator();
Set<ConstraintViolation<T>> failedValidations = validator.validate(object);

if (!failedValidations.isEmpty()) {
List<String> allErrors = failedValidations.stream().map(failure -> failure.getMessage())
.collect(Collectors.toList());
throw new ValidationsFatalException("Validation failure; Invalid request.", allErrors);
}
}
}

The Exception class

The ValidationsFatalException I used above is a custom exception class that extends RuntimeException. As you can see I am passing a message and a list of violations in case the DTO has more than one validation error.

public class ValidationsFatalException extends RuntimeException {
private String message;
private Throwable cause;
private List<String> details;

public ValidationsFatalException(String message, Throwable cause) {
super(message, cause);
}

public ValidationsFatalException(String message, Throwable cause, List<String> details) {
super(message, cause);
this.details = details;
}

public List<String> getDetails() {
return details;
}
}

Simulation of your scenario

In order to test whether this is working or not, I literally used your code to test and here is what I did

  • Create an endpoint as shown above
  • Autowire the CustomBeanValidator and trigger it's validateFields method passing the productRequest into it as shown below
  • Create a ProductRequest class as shown above
  • I annotated the productId with @NotNull and @Length(min=5, max=10)
  • I used Postman to make a GET request with a params having a value that is url-encoded json body

Assuming that the CustomBeanValidator is autowired in the controller, trigger the validation as follow after constructing the productRequest object.

beanValidator.validateFields(productRequest);

The above will throw exception if any violations based on annotations used.

How is the exception handled by exception controller?

As mentioned in the title, I use ExceptionController in order to handle the exceptions in my application.

Here is how the skeleton of my exception handler where the ValidationsFatalException maps to and then I update the message and set my desired status code based on exception type and return a custom object (i.e. the json you see below)

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({SomeOtherException.class, ValidationsFatalException.class})
public @ResponseBody Object handleBadRequestExpection(HttpServletRequest req, Exception ex) {
if(ex instanceof CustomBadRequestException)
return new CustomResponse(400, HttpStatus.BAD_REQUEST, ex.getMessage());
else
return new DetailedCustomResponse(400, HttpStatus.BAD_REQUEST, ex.getMessage(),((ValidationsFatalException) ex).getDetails());
}

Test 1

Raw params = {"productId":"abc123"}

Url encoded parmas = %7B%22productId%22%3A%22abc123%22%7D

Final URL: http://localhost:8080/app/product?params=%7B%22productId%22%3A%22abc123%22%7D

Result: All good.

Test 2

Raw params = {"productId":"ab"}

Url encoded parmas = %7B%22productId%22%3A%22ab%22%7D

Final URL: http://localhost:8080/app/product?params=%7B%22productId%22%3A%22ab%22%7D

Result:

{
"statusCode": 400,
"status": "BAD_REQUEST",
"message": "Validation failure; Invalid request.",
"details": [
"length must be between 5 and 10"
]
}

You can expand the Validator implementation to provide a mapping of field vs message error message.

Validation on loading JSON property with Spring Boot

you can validate your properties with JSR-303 annotations like that:

@Validated
@Component
@ConfigurationProperties(prefix = "my-config")
@RefreshScope
public class MyConfig {

@NotNull
@NotEmpty
@Size(min = 1, max = 5)
private List<Service> services;

private List<Consumer> consumers;
...

and so on.

@Validated enable validation every time annotated field get a value

Spring Validation without @Valid in Controller's handler methods and without implementing Validator interface?

Found the solution posted by this guy. Use this in the method:

javax.validation.Validator javaxValidator = javax.validation.Validation.buildDefaultValidatorFactory().getValidator();

org.springframework.validation.beanvalidation.SpringValidatorAdapter validator = new org.springframework.validation.beanvalidation.SpringValidatorAdapter(javaxValidator);

org.springframework.validation.Errors errors = new org.springframework.validation.BeanPropertyBindingResult.BeanPropertyBindingResult(objectThatNeedsToBeValidated, objectThatNeedsToBeValidated.getClass().getName());
validator.validate(objectThatNeedsToBeValidated, errors);

Edit: This is better than the above solution.



Related Topics



Leave a reply



Submit