@Valid when creating objects with jackson without controller
I have a model that I validate with @Valid in my Controllers when requests are send from the front-end:
@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;
but now I am also creating a object with Jackson's ObjectMapper without the controller. Is there a way to register this validation in the ObjectMapper or should I just check the variables in the setters?
Solution 1:
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
Solution 2:
I will also post how I managed to do it. Creating class that implements validator:
public class UserValidator implements Validator {
private static final int MINIMUM_NAME_LENGTH = 6;
@Override
public boolean supports(Class<?> clazz) {
return User.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "Name must be at least 7 characters long.");
User foo = (User) target;
if(foo.getGithub().length() > 0 && !extensionSpec.getGithub().matches("^(www|http:|https:)+//github.com/.+/.+$")){
errors.rejectValue("github", "Github must match http://github.com/:user/:repo");
}
if (errors.getFieldErrorCount("name") == 0 && foo.getName().trim().length() < MINIMUM_NAME_LENGTH) {
errors.rejectValue("name", "Name must be at least 7 characters");
}
}
}
Then creating databinder with the deserialized object, taking the binding result and then validating the object:
ObjectMapper mapper = new ObjectMapper();
User foo = mapper.readValue(FooJson, User.class);
Validator validator = new ObjectValidator();
BindingResult bindingResult = new DataBinder(foo).getBindingResult();
validator.validate(foo, bindingResult);
if(bindingResult.hasErrors()){
throw new BindException(bindingResult);
}
Also if you want to take the errorCodes in the body of the response:
@ExceptionHandler
ResponseEntity handleBindException(BindException e){
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(e.getBindingResult().getAllErrors()
.stream()
.map(DefaultMessageSourceResolvable::getCode)
.toArray());
}