Search code examples
spring-bootkotlinbean-validation

Request Body Validation on Class Level in Spring Boot


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.


Solution

  • 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:

    1. Add an attribute to your constraint annotation:
    @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 
    )
    
    1. Annotate your DTO using the new attribute and groups as:
    @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
        }
    }