Alexxxx
Alexxxx

Reputation: 67

Spring @Valid annotation on constructor not working. Best practice to validate Dto

Today I was looking for a way to validate a DTO that I have to return to the FE. I thought could be a good idea to create the constructor with the @Valid annotation from jakarta.validation package because documentation says it can be used on constructor. So I created my Test with an example DTO and the Lombok annotation on @AllArgsConstructor in this way:

@AllArgsConstructor(onConstructor_ = {@Valid})
public class TestValidatedDto {
    @NotNull
    private String value1;
    @NotBlank
    private String value2;
    @Min(2)
    @NotNull
    private Integer number;
}

This is the DTO generated by Lombok:

public class TestValidatedDto {
    private @NotNull String value1;
    private @NotBlank String value2;
    private @Min(2L) @NotNull Integer number;

    public @Valid TestValidatedDto(final String value1, final String value2, final Integer number) {
        this.value1 = value1;
        this.value2 = value2;
        this.number = number;
    }
}

And I have the @Valid annotation on the constructor as I was expecting. Now I created a Test but the result was not the one I needed.

@SpringBootTest
@Slf4j
class TestValidationAnnotationApplicationTests {

    @Autowired
    private Validator validator;

    @Test
    void testValidation1(){
        TestValidatedDto testValidatedDto = new TestValidatedDto(null, "     ", 1);
        log.info("Created dto: [{}]",testValidatedDto);
        Set<ConstraintViolation<TestValidatedDto>> violations = validator.validate(testValidatedDto);
        violations.forEach(v -> {
            log.info("VIOLATION: field [{}] - value: [{}] - message: [{}]", v.getPropertyPath(), v.getInvalidValue(), v.getMessage());
        });
    }

}

I can't understand why the @Valid on the constructor is not working. I know constructor is not a method, but the documentation says it can be used to validate the return from constructor. Does anyone know the right working of this annotation?

https://jakarta.ee/specifications/bean-validation/3.0/jakarta-bean-validation-spec-3.0.html

If this is not the right way to do it, can someone explain some best practice to validate Dto you need to return in controller in a simple way?

UPDATE: I forgot to say, I tried it also with the annotation @Validated on the DTO class, but the constructor validation still not work. This is the documentation part I want to verify:

Generic constraint annotations can target any of the following ElementTypes:

FIELD for constrained attributes

METHOD for constrained getters and constrained method return values

CONSTRUCTOR for constrained constructor return values

PARAMETER for constrained method and constructor parameters

TYPE for constrained beans

ANNOTATION_TYPE for constraints composing other constraints

TYPE_USE for container element constraints

Upvotes: 1

Views: 1350

Answers (2)

stackoe
stackoe

Reputation: 31

I valid success when use this test case, you can try my case.

DTO:

@Data
@AllArgsConstructor(onConstructor_ = {@Valid})
public class SimpleDTO {


  @NotNull(message = "corpId is null")
  private String corpId;

  @NotNull(message = "userId is null")
  private String userId;

  @NotNull(message = "deptCode is null")
  private String deptCode;

  @NotBlank
  private String notBlank;

}

TestCase:

 @SpringBootTest(classes = SimpleDemoApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
 class ValidTest {

    @Test
    void testValid() {
        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        LOG.info(validator.getClass().getName());
        SimpleDTO allArg = new SimpleDTO(null, null, null, "  ");
        Set<ConstraintViolation<Object>> checkResult = validator.validate(allArg);
        if (!CollectionUtils.isEmpty(checkResult)) {
            List<String> resultMsg =checkResult.stream().map(ConstraintViolation::getMessage).collect(Collectors.toList());
            throw new RuntimeException(JSON.toJSONString(resultMsg));
       }
    }


    @Autowired
    private Validator validator;

    private static final Logger LOG = getLogger(ValidTest.class);

    @Test
    @DisplayName("display injected validator SpringBean")
    void displayInjectValidatorBean(){
        LOG.info(validator.getClass().getName());
        SimpleDTO allArg = new SimpleDTO(null, null, null, "  ");
        Set<ConstraintViolation<Object>> checkResult = validator.validate(allArg);
        if (!CollectionUtils.isEmpty(checkResult)) {
            List<String> resultMsg = checkResult.stream().map(ConstraintViolation::getMessage).collect(Collectors.toList());
            throw new RuntimeException(JSON.toJSONString(resultMsg));
        }
    }

}

The injected Validator SpringBean is org.springframework.validation.beanvalidation.LocalValidatorFactoryBean. On this bean's comments documentation, I think this problem may be caused by Spring/hibernate-validator version.

------- Edit on 12/1 --------

I think you want to impl this:

public class SimpleDTO {

    private static final Logger LOG = getLogger(SimpleDTO.class);


    @NotNull(message = "corpId is null")
    private String corpId;

    @NotNull(message = "userId is null")
    private String userId;

    @NotNull(message = "deptCode is null")
    private String deptCode;

    @NotBlank
    private String notBlank;

    public SimpleDTO(String corpId, String userId, String deptCode, String notBlank) {
        this.corpId = corpId;
        this.userId = userId;
        this.deptCode = deptCode;
        this.notBlank = notBlank;
        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        Set<ConstraintViolation<SimpleDTO>> checkResult = validator.validate(this);
        checkResult.forEach(v -> LOG.error("VIOLATION: field [{}] - value: [{}] - message: [{}]", v.getPropertyPath(), v.getInvalidValue(), v.getMessage()));
}

}

On this case, it will valid before finish constructor init.You can think of it as a hook, when constructor finish.

But on this case, it changed the internal logic of the constructor. It is destructive.

Because this may change the constructor by writing directly to the bytecode

Upvotes: 0

bardelorean
bardelorean

Reputation: 11

Jakarta Bean Validation operates with a cascading nature. This means that @Valid doesn't trigger validation within the same object where it's placed when you create an object. For example, consider we have classes A and B:

class A {
    @NotBlank
    private String s;
    private B b;

    public A(String s, @Valid B b) {
        this.s = s;
        this.b = b;
    }
}

class B {
    @NotBlank
    private String s;
    @Min(2)
    private Integer n;

    public B(String s, Integer n) {
        this.s = s;
        this.n = n;
    }
}

When we instantiate class A, it will not check the @NotBlank constraint on the String s, but it will trigger the validation of the fields of the object B b.

Thus, @Valid is designed for cascading validation and is primarily used in controllers, where Spring automatically triggers validation. To validate your DTOs or any objects outside of such frameworks, you typically need to write specific code or manually use a Validator instance.

Upvotes: 0

Related Questions