Conditional Validating Request with SpringBoot - java

I am using Spring-WebMvc and SpringBoot in constructing a web service, together with spring-boot-starter-validation to simplify the setup.
In the new user requirement, validation needs to be done conditionally based on user request, which I am not sure how the design could fit well.
Below is an example, where such conditional checking is needed. Hope one can give a direction / solution on how can one do it in a better way.
Example
Let's say we are creating a service for multiple organizations and we are using one server to handle different organization request. Each organization request must supply a valid companyId to identify the organization.
And at the beginning, all organizations are strictly forbidding its own employee to place a lucky draw request.
public class Request {
#CompanyId
private String companyId;
}
#StaffNotAllowLuckyDraw
public class LuckyDrawRequest extends Request {
private String userId;
}
With the corresponding annotation and validator.
#Retention(RetentionPolicy.RUNTIME)
#Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
#Constraint(validatedBy = { StaffNotAllowLuckyDrawValidator.class })
public #interface StaffNotAllowLuckyDraw {
String message() default "{com.abc.validation.constraints.staffnotallowluckydraw.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class StaffNotAllowLuckyDrawValidator implements ConstraintValidator<StaffNotAllowLuckyDraw, LuckyDrawRequest> {
#Override
public boolean isValid(LuckyDrawRequest request, ConstraintValidatorContext context) {
String companyId = request.getCompanyId();
String userId = request.getUserId();
User user = Database.getUser(companyId, userId);
return !user.isStaff();
}
}
But later,
a new requirement comes, which allow company to customize the validation, allowing / forbidding a staff to place a lucky draw request.
So the validator logic needs to be changed.
And definitely I can add custom logic inside this validator to make it work very easily. Like below.
public class StaffNotAllowLuckyDrawValidator implements ConstraintValidator<StaffNotAllowLuckyDraw, LuckyDrawRequest> {
private List<String> exceptions;
#Override
public boolean isValid(LuckyDrawRequest request, ConstraintValidatorContext context) {
String companyId = request.getCompanyId();
if (exceptions.contains(companyId)) {
return true;
}
String userId = request.getUserId();
User user = Database.getUser(companyId, userId);
return !user.isStaff();
}
}
However, it seems unclear to me from the Annotation perspective (look at LuckyDrawRequest class) that there are such condition to skip such validation.
Worse still, there may have more and more such custom validation coming, and some maybe using default validation (e.g. #Max) Is there any approach to handle the above problem?
Note that the validation message should not be changed, on this change. As it has been used for months already.
P.S.
I am using spring-boot#2.5.3, which includes hibernate-validator#6.2.0, which should be JSR380 compliance.

Related

Add custom validation message to enum

Is it possible to add a custom message to an enum if validation fails?
I have this enum class:
public enum EngineType{
FOO('F'),
BAR('B'),
QUX('Q');
private char id;
EngineType(char id) {
this.id = id;
}
public char getId() {
return this.id;
}
}
My model class contains private MyEnum myEnum;.
Currently if a value is passed in which isn't a valid enum, I get this BindingException:
{
"status": 400,
"validationErrors": {
"myEnum": "Failed to convert property value of type 'java.lang.String' to required type 'ie.aviva.services.motor.cartellservice.model.EngineType' for property 'myEnum'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [#javax.validation.constraints.NotNull ie.aviva.services.motor.cartellservice.model.EngineType] for value 'corge';"
},
"title": "Bad Request"
}
My controller looks like this:
#RequestMapping(
method = RequestMethod.GET,
value = Endpoints.TRUE_MATCH,
produces = {"application/json"})
public ResponseEntity<ResponseWrapper<List<TrueMatch>>> getTrueMatch(
#Valid MyDetails MyDetails) {
LogContext.put(Constants.TAG, myDetails);
LOG.info(
"Controller called to get true match with my details: " + myDetails.toString());
...
}
MyDetails is like this:
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Setter
#SuperBuilder
public class MyDetails extends BasicDetails {
#NotBlank
#Pattern(
regexp = "^[a-zA-Z]*$",
message = "'${validatedValue}' contains unsupported characters")
private String name;
#NotNull private MyEnum myEnum;
...
}
Is it possible to change the message to some custom message of my own?
I'm already able to do this in annotations that I added to other variables by including the message parameter in the annotation. I tried creating an annotation to validate the pattern as seen here but it didn't work. I think because the annotation was added like this, which was too late. The exception had already been thrown:
#NotNull
#EnumNamePattern(regexp = "foo|bar|qux")
private MyEnum myEnum;
Sure. The simplistic solution would be to simply accept the value as a String, then wrap your parsing logic in a try-catch block, then return the result you want the user to see.
final MyEnum userValue;
try
{
userValue = MyEnum.valueOf(someStringInput);
}
catch (Exception e)
{
//respond to the user, either with an exception, or
//a proper response according to your application
}
Bean Validation & ControllerAdvice
You can make use of the Bean Validation and Exception-handling mechanisms provided by spring.
Note that OP is already using Bean validation in their project, which can be observed by the usage of annotations #NotBlank, #Pattern. So the following reference is exclusively for the Readers.
To include the Bean validation into your Spring Boot project, you can add Spring Boot Starter Validation dependency. In a nutshell, Bean validation is a specification (like for instance JPA) describing an IPA, which offers various annotations and interfaces for verifying that the data of domain objects is correct. And Hibernate Validator is an implementation of this specification which comes with Spring Boot Starter Validation.
To validate that the given String matches one of the enum-constants we would need a separate class, let's say MyDetailsDto. In case if you wonder why a new class? If the validation constant would be applied on the field of type enum it would validate only input that can be successfully parsed into an enum (otherwise an IllegalArgumentException would be thrown before applying the validation annotation), which can be useful only certain enum-constants can be assigned to a field. But that's not the case, we need to find out whether the given string is equal to one of the enum constants (by the way, in addition this approach creates an opportunity for sanitizing the input, for instance replacing some characters if needed).
#Getter
#Setter
public class MyDetailsDto {
private String name;
#EngineTypeConstraint
private String engineType;
public MyDetails toMyDetails() {
return new MyDetails(name, EngineType.valueOf(engineType));
}
}
So, to create a custom Validation constraint, we need two things: a custom annotation a Validator associated with it (for more information refer to the documentation - Configuring Custom Constraints):
Each bean validation constraint consists of two parts:
A #Constraint annotation that declares the constraint and its configurable properties.
An implementation of the jakarta.validation.ConstraintValidator interface that implements the constraint’s behavior.
We can find more information regarding the requirements for the custom annotation in the Bean validation documentation Constraint:
Each constraint annotation must host the following attributes:
String message() default [...]; which should default to an error
message key made of the fully-qualified class name of the constraint
followed by .message. For example
"{com.acme.constraints.NotSafe.message}"
Class<?>[] groups() default {}; for user to customize the targeted groups
Class<? extends Payload>[] payload() default {}; for extensibility purposes
So our custom annotation needs to have at least these three attributes, and we're interested primarily in the first one, providing a message which would be used in case if an exception occurs. Let's our annotation:
#Constraint(validatedBy = EngineTypeValidator.class)
#Documented
#Target({ElementType.METHOD, ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
public #interface EngineTypeConstraint {
String message() default "Engine type does not exist";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Validator should implement generic interface ConstraintValidator, specifying the associated annotation and the validated type as its parameters.
That's how the implementation might look like:
public class EngineTypeValidator implements ConstraintValidator<EngineTypeConstraint, String> {
private static final Set<String> TYPE_NAMES =
EnumSet.allOf(EngineType.class).stream().map(Enum::name).collect(Collectors.toSet());
#Override
public boolean isValid(String engineType,
ConstraintValidatorContext constraintValidatorContext) {
if (!TYPE_NAMES.contains(engineType)) throw new EngineTypeNotValidException(engineType);
return true;
}
}
As you have probably noticed, validator throws a custom exception EngineTypeNotValidException which receives an invalid engine model as a parameter. We need this exception in order to build a response based on it.
public class EngineTypeNotValidException extends RuntimeException {
public EngineTypeNotValidException(String type) {
super(String.format("Engine type '%s' doesn't exist.", type));
}
}
To handle this exception and produces an error-response by using Controller Advice. For that, we need to define a class annotated with #ControllerAdvice and create a method marked with #ExceptionHandler to target the custom exception defined above.
#ControllerAdvice(assignableTypes = MyDetailsController.class)
public class ValidationExceptionHandler {
#ExceptionHandler(EngineTypeNotValidException.class)
public ResponseEntity<ErrorResponse> handleInvalidEngine(RuntimeException e) {
Throwable cause = e.getCause();
return ResponseEntity
.badRequest()
.body(new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
HttpStatus.BAD_REQUEST.getReasonPhrase(),
List.of(cause.getMessage())
));
}
}
Finally, to customize the response body, we can create a POJO with a couple of string field for representing the information of a failing response.
#AllArgsConstructor
#Getter
public class ErrorResponse {
private int status;
private String message;
private List<String> errors;
}
Demo
That's it now we can give it a try.
Consider the following dummy Controller:
#RestController
public class MyDetailsController {
#PostMapping("/newMyDetails")
public String newCar(#RequestBody #Valid MyDetailsDto dto) {
// some business logic
return "is valid";
}
}
And here's a couple screenshots from Postman with responses:
Valid request (hard-coded message from the Controller in the response):
Invalid request (response with a customized error message, prompting that enum-name provided in the request doesn't exist):

Spring Boot - how to validate fields that depend on each other?

Is there some way in Spring Boot that I can perform validation on properties that depend on each other's values, and have the error message be associated with the property?
I want to return the errors to the user in a nice JSON structure:
{
"errors": {
"name": "is required if flag is true"
}
}
Example:
#Entity
public class MyEntity {
private boolean nameRequiredFlag;
// Required if "nameRequiredFlag" is set to true:
private String name;
}
One solution that doesn't solve my problem of associating the error message with the name property is to create a validator annotation for the entity:
#ValidEntity
public class MyEntity {
private boolean nameRequiredFlag;
// Required if "nameRequiredFlag" is set to true:
private String name;
}
#Constraint( validatedBy = { MyEntityValidator.class } )
#Documented
#Target( { ElementType.TYPE } )
#Retention( RetentionPolicy.RUNTIME )
public #interface ValidEntity{
Class<?>[] groups () default {};
String message () default "name is required if 'nameRequiredFlag' is true";
Class<? extends Payload>[] payload () default {};
}
public class MyEntityValidator implements Validator<ValidEntity, MyEntity> {
#Override
public boolean isValid ( MyEntity entity, ConstraintValidatorContext context ) {
if ( !entity.nameRequiredFlag ) return true;
return !StringUtils.isBlank( entity.getName() );
}
}
This is laughably cumbersome and doesn't solve my problem. Isn't there any way I can do this with the framework validation?
Edit: This is for a JSON API, and the consumer really needs to be able to associate the error message to a best guess at which field has an issue. It is not helpful to send the consumer an error message for the whole object, or a computed property.
Solution given by #EvicKhaosKat is one way of doing it. However, when there are too many fields dependent on each other in a complicated way, your class becomes full of annotations and I personally struggle a lot relating them.
A simpler approach is to create a method(s) in your pojo which does the cross field validations and returns a boolean. On the top of this method annotate it with #AssertTrue(message = "your message"). It will solve your problem in a cleaner fashion.
public class SampleClass {
private String duration;
private String week;
private String month;
#AssertTrue(message = "Duration and time attributes are not properly populated")
public boolean isDurationCorrect() {
if (this.duration.equalsIgnoreCase("month")) {
if (Arrays.asList("jan", "feb", "mar").contains(month))
return true;
}
if (this.duration.equalsIgnoreCase("week")) {
if (Arrays.asList("1-7", "8-15", "16-24", "25-31").contains(week))
return true;
}
return false;
}
}
Note: I have not tested this code but have used this approach in multiple places and it works.
Possible reason is that name validation operates on not-yet-fully constructed object, so nameRequiredFlag is not filled yet.
As an option there is a #GroupSequence annotation, which allows to group and perform validations in an order you specify.
For example it is possible to add to MyEntity annotations:
#ValidEntity(groups = DependentValidations.class)
#GroupSequence({MyEntity.class, DependentValidations.class})
So all the other validation annotations on MyEntity class gonna be performed first, and after that DependentValidations group, which consists of ValidEntity.
Thus ValidEntity will be called on fully created object, and the last in order.
(DependentValidations.class - just an empty interface created somewhere nearby, like any other marker interface)
https://www.baeldung.com/javax-validation-groups will possibly describe that in much more details.
p.s. answer provided by #Innovationchef will possibly suit the case more :)

How can a Spring Boot web app validate a form field that is required conditional on another field?

I have a Spring Boot web app in which fields of my form-backing bean are annotated with Bean Validation annotations (see Baeldung's tutorial or the docs at spring.io). For example, fields on Customer beans might be annotated like this:
#NotBlank
#Pattern(regexp="[ \\.A-Za-z-]*")
private String firstName;
#DateTimeFormat(pattern="M/d/yyyy")
#NotNull
#Past
private Date DOB;
What I want to know is: (How) Can I implement a complex validation that looks at multiple fields? I mean, using this framework. For example, let's say I have a field Country and a field ZipCode and I want the ZipCode to be #NotBlank if and only if the Country equals "US", optional otherwise.
In my Controller, the validation is very elegantly triggered with the use of the #Valid annotation, and errors are attached to a BindingResult object. Like so:
#PostMapping("customer/{id}")
public String updateCustomer( #PathVariable("id") Integer id,
#Valid #ModelAttribute("form") CustomerForm form,
BindingResult bindingResult ) {
if (bindingResult.hasErrors()) {
return "customerview";
}
customerService.updateCustomer(form);
return "redirect:/customer/"+id;
}
What I'm hoping to find is a way to write this conditional validator in a way that it, too, will be triggered by the #Valid annotation and will attach it's error message to the ZipCode field in the BindingResult. Ideally I shouldn't have to change the Controller code at all.
you can getting help of creating custom validator class, the following link helps you:
https://www.baeldung.com/spring-mvc-custom-validator#custom-validation
For this kind of stuff you need to use cross field validation. Please try :
Create annotation :
package foo.bar;
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {CustomerValidator.class})
public #interface CustomerValid {
String message() default "{foo.bar.CustomerValid.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Create custom validator :
public class CustomerValidator implements ConstraintValidator<CustomerValid, Customer> {
#Override
public void initialize(CustomerValid constraint) {
}
#Override
public boolean isValid(Customer customer, ConstraintValidatorContext context) {
return !("US".equals(customer.getCountry() && "".equals(customer.getZipCode())));
}
}
Annotate your class :
#CustomerValid
public class Customer {
// body
}
This class validation will be processed in addition to existent field validators.

Dynamic POJO validation based on groups in spring

Consider the following pojo for reference:
public class User{
private String username;
private String firstName;
private String middleName;
private String lastName;
private String phone;
//getters and setters
}
My application is a basically spring-boot based REST API which exposes two endpoints, one to create the user and the other to retrieve a user.
The "users" fall into certain categories, group-a, group-b etc. which I get from the headers of the post request.
I need to validated the user data in runtime and the validations may differ based on the group of a user.
for example, the users that fall into group-a may have phone numbers as an optional field whereas it might be a mandatory field for some other group.
The regex may also vary based on their groups.
I need to be able to configure spring, to somehow dynamically validate my pojo as soon as they are created and their respective set of validations get triggered based on their groups.
Maybe I can create a yml/xml configuration which would allow me to enable this?
I would prefer to not annotate my private String phone with #NotNull and #Pattern.
My configuration is as follows:
public class NotNullValidator implements Validator {
private String group;
private Object target;
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public Object getTarget() {
return target;
}
public void setTarget(Object target) {
this.target = target;
}
#Override
public void validate(Object o) {
if (Objects.nonNull(o)) {
throw new RuntimeException("Target is null");
}
}
}
public interface Validator {
void validate(Object o);
}
#ConfigurationProperties(prefix = "not-null")
#Component
public class NotNullValidators {
List<NotNullValidator> validators;
public List<NotNullValidator> getValidators() {
return validators;
}
public void setValidators(List<NotNullValidator> validators) {
this.validators = validators;
}
}
application.yml
not-null:
validators:
-
group: group-a
target: user.username
-
group: group-b
target: user.phone
I want to configure my application to somehow allow the validators to pick their targets (the actual objects, not the strings mentioned in the yml), and invoke their respective public void validate(Object o) on their targets.
P.S.
Please feel free to edit the question to make it better.
I am using jackson for serializing and deserializing JSON.
The easiest solution to your problem, as i see it, is not with Spring or the POJOs themselves but with a design pattern.
The problem you're describing is easily solved by a strategy pattern solution.
You match the strategy to use by the header you're expecting in the request, that describes the type of user, and then you perform said validations inside the strategy itself.
This will allow you to use the same POJO for the whole approach, and deal with the specifics of handling/parsing and validating data according to the each type of user's strategy.
Here's a link from wiki books with a detailed explanation of the pattern
Strategy Pattern
Suppose you have a basic interface for your strategies:
interface Strategy {
boolean validate(User user);
}
And you have 2 different implementations for the 2 different types of user:
public class StrategyA implements Strategy {
public boolean validate(User user){
return user.getUsername().isEmpty();
}
}
public class StrategyB implements Strategy {
public boolean validate(User user){
return user.getPhone().isEmpty();
}
}
You add a Strategy attribute to your User POJO and assign the right implementation of the Strategy to that attribute when you receive the post request.
Everytime you need to validate data for that user you just have to invoke the validate method of the assigned strategy.
If each User can fit multiple strategies, you can add a List<Strategy> as an attribute instead of a single one.
If you don't want to change the POJO you have to check which is the correct strategy every time you receive a post request.
Besides the validate method you can add methods to handle data, specific to each strategy.
Hope this helps.
You can use validation groups to control which type of user which field gets validated for. For example:
#NotBlank(groups = {GroupB.class})
private String phone;
#NotBlank(groups = {GroupA.class, GroupB.class})
private String username;
Then you use the headers from the request that you mentioned to decide which group to validate against.
See http://blog.codeleak.pl/2014/08/validation-groups-in-spring-mvc.html?m=1 for a complete example.
Updated to include a more comprehensive example:
public class Val {
private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
public boolean isValid(User user, String userType) {
usergroups userGroup = usergroups.valueOf(userType);
Set<ConstraintViolation<User>> constraintViolations = validator.validate(user, userGroup.getValidationClass());
return constraintViolations.isEmpty();
}
public interface GroupA {}
public interface GroupB {}
public enum usergroups {
a(GroupA.class),
b(GroupB.class);
private final Class clazz;
usergroups(Class clazz) {
this.clazz = clazz;
}
public Class getValidationClass() {
return clazz;
}
}
}
This doesn't use application.yaml, instead the mapping of which fields are validated for each group is set in annotations, similar results using Spring's built in validation support.
I was able to solve my problem with the use of Jayway JsonPath.
My solution goes as follows:
Add a filter to your API which has the capability to cache the InputStream of the ServletRequest since it can be read only once. To achieve this, follow this link.
Create a bunch of validators and configure them in your application.yml file with the help of #ConfigurationProperties. To achieve this, follow this link
Create a wrapper which would contain all your validators as a list and initialize it with #ConfigurationProperties and the following configuration:
validators:
regexValidators:
-
target: $.userProfile.lastName
pattern: '[A-Za-z]{0,12}'
group: group-b
minMaxValidators:
-
target: $.userProfile.age
min: 18
max: 50
group: group-b
Call the validate method in this wrapper with the group which comes in the header, and then call the validate of the individual validators. To achieve this, I wrote the following piece of code in my wrapper:
public void validate(String input, String group) {
regexValidators.stream()
.filter(validator -> group.equals(validator.getGroup()))
.forEach(validator -> validator.validate(input));
minMaxValidators.stream()
.filter(validator -> group.equals(validator.getGroup()))
.forEach(validator -> validator.validate(input));
}
and the following method in my validator:
public void validate(String input) {
String data = JsonPath.parse(input).read(target);
if (data == null) {
throw new ValidationException("Target: " + target + " is NULL");
}
Matcher matcher = rule.matcher(data);
if (!matcher.matches()) {
throw new ValidationException("Target: " + target + " does not match the pattern: " + pattern);
}
}
I have created a functioning project to demonstrate the validations and it can be found here.
I understand that the answer alone might not be very clear, please follow the above mentioned url for the complete source code.

jOOQ: Allowed-Character constraints?

I am considering moving from Hibernate to jOOQ but I can't find e.g.
how to have Pattern-Constraints on a String like this in Hibernate:
#NotEmpty(message = "Firstname cannot be empty")
#Pattern(regexp = "^[a-zA-Z0-9_]*$", message = "First Name can only contain characters.")
private String firstname;
How would I do that in jOOQ?
The "jOOQ way"
The "jOOQ way" to do such validation would be to create either:
A CHECK constraint in the database.
A trigger in the database.
A domain in the database.
After all, if you want to ensure data integrity, the database is where such constraints and integrity checks belong (possibly in addition to functionally equivalent client-side validation). Imagine a batch job, a Perl script, or even a JDBC statement that bypasses JSR-303 validation. You'll find yourself with corrupt data in no time.
If you do want to implement client-side validation, you can still use JSR-303 on your DTOs, which interact with your UI, for instance. But you will have to perform validation before passing the data to jOOQ for storage (as artbristol explained).
Using a Converter
You could, however, use your own custom type by declaring a Converter on individual columns and by registering such Converter with the source code generator.
Essentially, a Converter is:
public interface Converter<T, U> extends Serializable {
U from(T databaseObject);
T to(U userObject);
Class<T> fromType();
Class<U> toType();
}
In your case, you could implement your annotations as such:
public class NotEmptyAlphaNumericValidator implements Converter<String, String> {
// Validation
public String to(String userObject) {
assertNotEmpty(userObject);
assertMatches(userObject, "^[a-zA-Z0-9_]*$");
return userObject;
}
// Boilerplate
public String from(String databaseObject) { return databaseObject; }
public Class<String> fromType() { return String.class; }
public Class<String> toType() { return String.class; }
}
Note that this is more of a workaround, as Converter hasn't been designed for this use-case, even if it can perfectly implement it.
Using formal client-side validation
There's also a pending feature request #4543 to add more support for client-side validation. As of jOOQ 3.7, this is not yet implemented.
I recommend you don't try to use jOOQ in a 'hibernate/JPA' way. Leave the jOOQ generated classes as they are and map to your own domain classes manually, which you are free to annotate however you like. You can then call a JSR validator before you attempt to persist them.
For example, jOOQ might generate the following class
public class BookRecord extends UpdatableRecordImpl<BookRecord> {
private String firstname;
public void setId(Integer value) { /* ... */ }
public Integer getId() { /* ... */ }
}
You can create your own domain object
public class Book {
#NotEmpty(message = "Firstname cannot be empty")
#Pattern(regexp = "^[a-zA-Z0-9_]*$", message = "First Name can only contain characters.")
private String firstname;
public void setId(Integer value) { /* ... */ }
public Integer getId() { /* ... */ }
}
and map by hand once you've retrieved a BookRecord, in your DAO layer
Book book = new Book();
book.setId(bookRecord.getId());
book.setFirstname(bookRecord.getFirstname());
This seems quite tedious (and ORM tries to spare you this tedium) but actually it scales quite well to complicated domain objects, in my opinion, and it's always easy to figure out the flow of data in your application.

Categories

Resources