Request Body Validation on Class Level in Spring Boot

66 Views Asked by At

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.

1

There are 1 best solutions below

3
mark_o On BEST ANSWER

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
    }
}