I have a dto class and I am applying a class level custom constraint to it. My problem is I want it to behave differently in case of create and update apis. In case of property validation it is easy to achieve with different annotations but on class level I am having some issues about finding a proper solution. Here is a minimalistic example of what I am trying to achieve:
interface Create
interface Update
@ValidApiDto(groups = [Create::class, Update::class])
data class ApiDto(
val id: Long,
val metaData: MetaDataDto,
// many other properties
)
@Constraint(validatedBy = [ApiDtoValidator::class])
@Target(allowedTargets = [AnnotationTarget.CLASS])
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class ValidApiDto(
val message: String = "Invalid api dto!",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
)
class ApiDtoValidator: ConstraintValidator<ValidApiDto, ApiDto> {
override fun isValid(dto: ApiDto, context: ConstraintValidatorContext): Boolean {
// My logic comes here. Adapt behavior based on update or create
return true
}
}
@RestController
@Validated
class MyRestController {
@PostMapping("/post")
@Validated(value = [Create::class])
fun post(@Valid dto: ApiDto): ResponseEntity<*>? {
return null
}
@PutMapping("/put")
@Validated(value = [Update::class])
fun update(@Valid dto: ApiDto): ResponseEntity<*>? {
return null
}
}
Is it possible to have different logics in one validator?
I tried to find the passed group by the context but it gives me all possible groups, which are create and update in my case.
Validation groups is the way to filter constraints, i.e. you are grouping constraints into groups and, depending on a condition, execute the constraints from a group.
If you'd want to have a conditional validator implementation what you can do instead is:
@Repeatable
@Constraint(validatedBy = [ApiDtoValidator::class])
@Target(allowedTargets = [AnnotationTarget.CLASS])
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class ValidApiDto(
val message: String = "Invalid api dto!",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = [],
// add some enum, or a boolean, whatever you find more suitable:
val type: ConstraintType = ConstraintType.CREATE
)
@ValidApiDto(type = ConstraintType.CREATE, groups = [Create::class])
@ValidApiDto(type = ConstraintType.UPDATE, groups = [Update::class])
data class ApiDto(
val id: Long,
val metaData: MetaDataDto,
// many other properties
)
This way you are assigning constraints with a different configuration to a different validation group and only a corresponding constraint will be applied in that group.
In your implementation of constraint validator you'll have access to the annotation attribute:
class ApiDtoValidator: ConstraintValidator<ValidApiDto, ApiDto> {
override fun initialize(ValidApiDto constraintAnnotation) {
// access your configured type value:
val type constraintAnnotation.type();
}
override fun isValid(dto: ApiDto, context: ConstraintValidatorContext): Boolean {
// My logic comes here. Adapt behavior based on update or create
return true
}
}