I am trying to create a UniqueName
annotation as a cutomize bean validation annotation for a create project api:
@PostMapping("/users/{userId}/projects")
public ResponseEntity createNewProject(@PathVariable("userId") String userId,
@RequestBody @Valid ProjectParam projectParam) {
User projectOwner = userRepository.ofId(userId).orElseThrow(ResourceNotFoundException::new);
Project project = new Project(
IdGenerator.nextId(),
userId,
projectParam.getName(),
projectParam.getDescription()
);
...
}
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
class ProjectParam {
@NotBlank
@NameConstraint
private String name;
private String description;
}
@Constraint(validatedBy = UniqueProjectNameValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
public @interface UniqueName {
public String message() default "already existed";
public Class<?>[] groups() default {};
public Class<? extends Payload>[] payload() default{};
}
public class UniqueProjectNameValidator implements ConstraintValidator<UniqueName, String> {
@Autowired
private ProjectQueryMapper mapper;
public void initialize(UniqueName constraint) {
}
public boolean isValid(String value, ConstraintValidatorContext context) {
// how can I get the userId info??
return mapper.findByName(userId, value) == null;
}
}
The problem is that name
field just need uniqueness for user level. So I need to get the {userId}
from the URL field for validation. But how can I add this into the UniqueProjectNameValidator
? Or is there some better way to handle this validation? This is just a small part of a large object, the real object has many other complex validations in the request handler which make the code quite dirty.
As @Abhijeet mentioned, dynamically passing the userId
property to the constraint validator is impossible. As to how to handle this validation case better, there's the clean solution and the dirty solution.
The clean solution is to extract all the business logic to a service method, and validate the ProjectParam
at the service level. This way, you can add a userId
property to ProjectParam
, and map it from the @PathVariable
onto the @RequestBody
before calling the service. You then adjust UniqueProjectNameValidator
to validate ProjectParam
s rather than String
s.
The dirty solution is to use Hibernate Validator's cross-parameter constraints (see also this link for an example). You essentially treat both of your controller method parameters as the input for your custom validator.