Cascaded bean validation 2.0 not working with nested object inside Map
Asked Answered
S

2

7

Although, this question has been answered I'm interested why @Validated is needed for a working cascaded validation of Map<String, @Valid Employee>.

Update 2: For some deeper understanding I've found those posts (One,Two and Three), which explains, that @Validated is neeeded to activate method level validation. With the help of this, collections can be validated, due they are no JavaBeans which are validated instead (JSR 303).


Solution: I've updated my code snippets and my repository with working code examples. All I have to do is to annotate my controller with @Validated and add some getters in Employee. MethodValidationPostProcessor is not necessary at all.

Update: I've updated my question and forked Spring Boot Rest example to add a minimal Rest API to demonstrate:

Github Repo. The example values are inside README.md!


I've got an Spring Boot 2 API to store some employees. I can pass either one Employee or either a Map<String, Employee>.

@Validated //this is the solution to activate map validation
@RestController
class EmployeeController {

  @PostMapping("/employees")
  List<Employee> newEmployee(@RequestBody @Valid Employee newEmployee) {
     ...
  }

  @PostMapping("/employees/bulk")
  List<Employee> newEmployee(@RequestBody Map<String, @Valid Employee> 
  newEmployees) {
     ...
  }
}

Employee exists of some inner static classes which also needs to be validated:

public class Employee {

    @NotBlank
    public final String name;
    @Valid
    public final EmployeeRole role;

    @JsonCreator
    public Employee(@JsonProperty("name") String name,
        @JsonProperty("role") EmployeeRole role) {

        this.name = name;
        this.role = role;
    }

    // getters

    public static class EmployeeRole {

        @NotBlank
        public String rolename;

        @Min(0)
        public int rating;

        @JsonCreator
        public EmployeeRole(@JsonProperty("rolename") String rolename,
            @JsonProperty("rating") int rating) {

            this.rolename = rolename;
            this.rating = rating;
        }

        // getters
    }
}


For now, validation for single requests are working but not for my bulk requests. As far as i know this should be possible with Bean validation 2.0.

Do you know what I've did wrong? Do i need to write a custom validator?

Saporous answered 11/2, 2020 at 11:35 Comment(2)
Try remove @NotNull from CalculationRequestContext.Hygrophilous
I don't why this should help, maybe some explanation? However, it did not help at all.Saporous
I
3

To make it working you have to do following:

Add MethodValidationPostProcessor bean to configuration

@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
    return new MethodValidationPostProcessor();
}

Add @Validated to your EmployeeController

@Validated
@RestController
public class EmployeeController {}'

Add @Valid to Map or to Employee

public List<Employee> newEmployee(@RequestBody @Valid Map<String, Employee> newEmployees) {}   
public List<Employee> newEmployee(@RequestBody Map<String, @Valid Employee> newEmployees) {}

That's all. This is entire EmployeeController:

@Validated
@RestController
public class EmployeeController {

    @PostMapping("/employees")
    public List<Employee> newEmployee(@RequestBody @Valid Employee newEmployee) {
        return Collections.singletonList(newEmployee);
    }

    @PostMapping("/employees/bulk")
    public List<Employee> newEmployee(@RequestBody @Valid Map<String, Employee> newEmployees) {
        return new ArrayList<>(newEmployees.values());
    }
}

And SpringBoot configuration file

@SpringBootApplication
public class Application extends SpringBootServletInitializer {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }

}

Hope it help you.

Institutionalize answered 11/2, 2020 at 16:42 Comment(4)
@Saporous right. I have updated the answer. I have checked - it works to me.Institutionalize
This is working! Can you tell me why i do need this instead of working Bean Validation 2 out of the box?Saporous
I can’t. I do not know all Spring internals. I suppose this is not the one way, should be another one.Institutionalize
Okay, however thanks for the answer. Maybe somebody else could answer this. Fyi: MethodValidationPostProcessor is not necessary at all.Saporous
E
3

There are two kinds of validation in spring system.

  • A: The spring boot controller methods parameter validation, only works for the http post request body data in controller with @Valid or @Validated aside
  • B: The method level validation, works for any method parameters and return values with @Validated on class and @Valid aside values to be validated

We can see that A is more narrow while B is a more common one. I'd like to answer the question on two aspects.

1 Answers are in the code

As describe in this post, the more detail part, A and B triggers method enhancement via aop by calling different method in org.hibernate.validator.internal.engine.ValidatorImpl, which leads to the difference.

  • A call validate method in ValidatorImpl via RequestResponseBodyMethodProcessor
  • B call call validateParameters method in ValidatorImpl via MethodValidationInterceptor

They are different methods with different functions, so lead to different results. You can find the answer by reading the two methods.

2 Answers are in the specification

The JSR-303 defines functions of the methods we discussed above.

validate method is explained in the validation method part, and the implementation must obey the logic defined in validation routine, in which it states that it will execute all the constraint validation for all reachable fields of the object, this is why element of List object (or other collection instance) cannot be validated via this method - the elements of the collection are not fields of the collection instance.

But validateParameters, JSR-303 actually doesn't treat it as main topic and put it in Appendix C. Proposal for method-level validation. It provides some description:

The constraints declarations evaluated are the constraints hosted on the parameters of the method or constructor. If @Valid is placed on a parameter, constraints declared on the object itself are considered.

validateReturnedValue evaluates the constraints hosted on the method itself. If @Valid is placed on the method, the constraints declared on the object itself are considered.

public @NotNull String saveItem(@Valid @NotNull Item item, @Max(23) BigDecimal price)

In the previous example,

- item is validated against @NotNull and all the constraints it hosts
- price is validated against @Max(23)
- the result of saveItem is validated against @NotNull

and exclaim that Bean Validation providers are free to implement this proposal as a specific extension. As far as I know, the Hibernate Validation project implements this method, makes constraints works on the object itself, and element of collection object.

3 Some complain

I don't know why the spring framework guys call validate in RequestResponseBodyMethodProcessor, makes lots of related questions appeare in stackoverflow. Maybe it's just because http post body data usually is a form data, and can be represented by a java bean naturally. If it's me, I'll call the validateParametes in RequestResponseBodyMethodProcessor for easy use.

Eaglet answered 12/2, 2020 at 15:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.