@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'svalidateFields
method passing theproductRequest
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 aGET
request with aparams
having a value that isurl-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
How Read Doc or Docx File in Java
Which Is More Effective: If (Null == Variable) or If (Variable == Null)
Java 8 Localdatetime Is Parsing Invalid Date
Start Mail-Client with Attachment
How to Securely Store Encryption Keys in Java
Spring Scheduling Task - Run Only Once
N-Gram Generation from a Sentence
Checked VS Unchecked Exceptions in Java
How to Use a Variable of One Method in Another Method
Java.Sql.Sqlexception: No Suitable Driver Found for Jdbc:Mysql://Localhost:3306/Dbname
Calling Base Class Overridden Function from Base Class Method
Import Com.Sun.Image.Codec.Jpeg.*
Gwt: Timer and Scheduler Classes
Using Variables Outside of an If-Statement
Jackson Change JSONignore Dynamically
Error 415 Unsupported Media Type: Post Not Reaching Rest If JSON, But It Does If Xml