spring-bootkotlinspring-webfluxkotlin-coroutineshibernate-validator

failed to validate request params in a Spring Boot/Kotlin Coroutines controller


In a SpringBoot/Kotlin Coroutines project, I have a controller class like this.


@RestContollser
@Validated
class PostController(private val posts: PostRepository) {

    suspend fun search(@RequestParam q:String, @RequestParam  @Min(0) offset:Int, @RequestParam  @Min(1) limit:Int): ResponseEntity<Any> {}

}

The validation on the @ResquestBody works as the general Spring WebFlux, but when testing

validating request params , it failed and throws an exception like:

java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
    at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)
    Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below:

It is not a ConstraintViolationException.


Solution

  • Thanks for the happy songs' comments, I found the best solution by now to overcome this barrier from the Spring Github issues#23499.

    As explained in comments of this issue and PaulNuk's answer, there is a Continuation will be appended to the method arguments at runtime, which will fail the index computation of the method parameter names in the Hibernate Validator.

    The solution is changing the ParameterNameDiscoverer.getParameterNames(Method) method and adding a empty string as the additional parameter name when it is a suspend function.

    class KotlinCoroutinesLocalValidatorFactoryBean : LocalValidatorFactoryBean() {
        override fun getClockProvider(): ClockProvider = DefaultClockProvider.INSTANCE
    
        override fun postProcessConfiguration(configuration: javax.validation.Configuration<*>) {
            super.postProcessConfiguration(configuration)
    
            val discoverer = PrioritizedParameterNameDiscoverer()
            discoverer.addDiscoverer(SuspendAwareKotlinParameterNameDiscoverer())
            discoverer.addDiscoverer(StandardReflectionParameterNameDiscoverer())
            discoverer.addDiscoverer(LocalVariableTableParameterNameDiscoverer())
    
            val defaultProvider = configuration.defaultParameterNameProvider
            configuration.parameterNameProvider(object : ParameterNameProvider {
                override fun getParameterNames(constructor: Constructor<*>): List<String> {
                    val paramNames: Array<String>? = discoverer.getParameterNames(constructor)
                    return paramNames?.toList() ?: defaultProvider.getParameterNames(constructor)
                }
    
                override fun getParameterNames(method: Method): List<String> {
                    val paramNames: Array<String>? = discoverer.getParameterNames(method)
                    return paramNames?.toList() ?: defaultProvider.getParameterNames(method)
                }
            })
        }
    }
    
    class SuspendAwareKotlinParameterNameDiscoverer : ParameterNameDiscoverer {
    
        private val defaultProvider = KotlinReflectionParameterNameDiscoverer()
    
        override fun getParameterNames(constructor: Constructor<*>): Array<String>? =
            defaultProvider.getParameterNames(constructor)
    
        override fun getParameterNames(method: Method): Array<String>? {
            val defaultNames = defaultProvider.getParameterNames(method) ?: return null
            val function = method.kotlinFunction
            return if (function != null && function.isSuspend) {
                defaultNames + ""
            } else defaultNames
        }
    }
    

    Then declare a new validator factory bean.

        @Primary
        @Bean
        @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
        fun defaultValidator(): LocalValidatorFactoryBean {
            val factoryBean = KotlinCoroutinesLocalValidatorFactoryBean()
            factoryBean.messageInterpolator = MessageInterpolatorFactory().getObject()
            return factoryBean
        }
    

    Get the complete sample codes from my Github.

    Update: Spring 6 has built-in Validation Support for Kotlin Coroutines Controller.