Reputation: 67
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
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
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