JSR-303 validation groups define a default group
Asked Answered
P

3

34

I have a bean that has a lot of fields annotated with JSR-303 validation annotations. There is a new requirement now that one of the fields is mandatory, but only in certain conditions.

I looked around and have found what I needed, validation groups.

This is what I have now:

public interface ValidatedOnCreationOnly {
}

@NotNull(groups = ValidatedOnCreationOnly.class)
private String employerId;
@Length(max = 255)
@NotNull
private String firstName;
@Length(max = 255)
@NotNull
private String lastName;

However, when I run this validation in a unit test:

@Test
public void testEmployerIdCanOnlyBeSetWhenCreating() {
    EmployeeDTO dto = new EmployeeDTO();

    ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
    Set<ConstraintViolation<EmployeeDTO>> violations = vf.getValidator().validate(dto, EmployeeDTO.ValidatedOnCreationOnly.class);

    assertEquals(violations.size(), 3);
}

It turns out that all of the non-group annotated validations are ignored and I get only 1 violation.

I can understand this behaviour but I would like to know if there is a way I can make the group include all non-annotated parameters as well. If not I'd have to do something like this:

public interface AlwaysValidated {
}

public interface ValidatedOnCreationOnly extends AlwaysValidated {
}

@NotNull(groups = ValidatedOnCreationOnly.class)
private String employerId;
@Length(max = 255, groups = AlwaysValidated.class)
@NotNull(groups = AlwaysValidated.class)
private String firstName;
@Length(max = 255, groups = AlwaysValidated.class)
@NotNull(groups = AlwaysValidated.class)
private String lastName;

The real class I'm working with has a lot more fields (about 20), so this method turns what was a clear way of indicating the validations into a big mess.

Can anyone tell me if there is a better way? Maybe something like:

vf.getValidator().validate(dto, EmployeeDTO.ValidatedOnCreationOnly.class, NonGroupSpecific.class);

I'm using this in a spring project so if spring has another way I'll be glad to know.

Plasm answered 12/2, 2016 at 8:59 Comment(1)
On controller @Validated( {ValidatedOnCreationOnly.class,Default.class}Marquise
H
57

There is a Default group in javax.validation.groups.Default, which represents the default Bean Validation group. Unless a list of groups is explicitly defined:

  • constraints belong to the Default group
  • validation applies to the Default group

You could extends this group:

public interface ValidatedOnCreationOnly extends Default {}
Hartwig answered 12/2, 2016 at 10:15 Comment(1)
nice answer. just wanted to know if we create an interface and extend it with Default, then how spring knows that a particular validation group will not perform validation on that particular field.Willis
F
3

just wanted to add more:

if you're using spring framework you can use org.springframework.validation.Validator

@Autowired
private Validator validator;

and to perform validation manually:

validator.validate(myObject, ValidationErrorsToException.getInstance());

and in controller:

@RequestMapping(method = RequestMethod.POST)
public Callable<ResultObject> post(@RequestBody @Validated(MyObject.CustomGroup.class) MyObject request) {
    // logic
}

although in this way extending from javax.validation.groups.Default won't work so you have to include Default.class in groups:

class MyObject {

    @NotNull(groups = {Default.class, CustomGroup.class})
    private String id;

    public interface CustomGroup extends Default {}
}
Formyl answered 13/12, 2016 at 6:32 Comment(0)
K
3

For me add Default.class everywhere is not good approach. So I extended LocalValidatorFactoryBean which validate with some group and delegate for validation without any group.

I used spring boot 2.2.6.RELEASE and I used spring-boot-starter-validation dependency.

My bean for validattion

public class SomeBean {

    @NotNull(groups = {UpdateContext.class})
    Long id;
    @NotNull
    String name;
    @NotNull
    String surName;
    String optional;
    @NotNull(groups = {CreateContext.class})
    String pesel;
    @Valid SomeBean someBean;
}

code of own class which extends LocalValidatorFactoryBean

public class CustomValidatorFactoryBean extends LocalValidatorFactoryBean {

    @Override
    public void validate(Object target, Errors errors, Object... validationHints) {
        if (validationHints.length > 0) {
            super.validate(target, errors, validationHints);
        }
        super.validate(target, errors);
    }
}

Put it to spring context via @Bean or just with @Component (as you wish)

    @Bean
    @Primary
    public LocalValidatorFactoryBean customLocalValidatorFactoryBean() {
        return new CustomValidatorFactoryBean();
    }

usage of it in some RestController

// So in this method will do walidation on validators with CreateContext group and without group
    @PostMapping("/create")
    void create(@RequestBody @Validated(CreateContext.class) SomeBean someBean) {

    }

    @PostMapping("/update")
    void update(@RequestBody @Validated(UpdateContext.class) SomeBean someBean) {

    }

Due to some reason testValidation is not working when is invoked DummyService.testValidation() by RestController or other spring bean. Only on RestController side is working :/

@Validated
@Service
@AllArgsConstructor
class DummyService {
  public void testValidation(@NotNull String string, @Validated(UpdateContext.class) SomeBean someBean) {
        System.out.println(string);
        System.out.println(someBean);
    }
}
Kolosick answered 9/3, 2021 at 8:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.