Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin and Spring Boot Request Body Validation

I'm just starting to use kotlin along side with Spring Boot to develop a simple web application.

Let´s take a simple data class object

@Entity  
data class User (name: String) {  
  @Id @GeneratedValue(strategy = GenerationType.AUTO)  
  var id: Long = -1  
}  

and a Controller

@RequestMapping(method = arrayOf(RequestMethod.POST))
fun createUser (@RequestBody user: User): User {
    return userService.createUser(user)
}

Well, making a request with any request body would just throw an http 400 error no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?); nested exception is com.fasterxml.jackson.databind.JsonMappingException

To eliminate this error I found out that we need to give some default values to the constructor arguments so:

name: String = ""

or maybe:

name: String? = null

Now, there is absolutely no validation of the input that goes on the response body, meaning that, if the JSON present in there does not respect the syntax of the User class, the default values will be used and stored in the database.

Is there any way to validate the request body JSON input to throw a bad request if it does not comply with the User class arguments without having to do it manually?

In this example there is only one argument but with larger classes manually does not seems like a good way to go

Thanks in advance

like image 984
Miguel Anciães Avatar asked Nov 07 '22 10:11

Miguel Anciães


1 Answers

I think the following solution that consist of two parts could take place for Spring Boot 2.x:


API facade configuration

@Configuration
class ValidationRouter(
        val validationService: ValidationService
) {

    @Bean
    fun router() = router {
        accept(APPLICATION_JSON).nest {
            "/api".nest {
                POST("/validation") { req ->
                    req.bodyToMono(ValidatingBody::class.java)
                            .doOnNext(validationService::validate)
                            .flatMap { ServerResponse.ok().build() }
                            .onErrorResume(BodyValidationException::class.java, validationService::handle)
                }
            }

        }

    }

    data class ValidatingBody(
            @get:NotBlank(message = "should not be blank")
            var mandatoryText: String?,
            @get:Size(min = 2, max = 10, message = "should contain more than 2 and less than 10 characters")
            @get:NotBlank(message = "should not be blank")
            var sizedText: String?,
            @get:Min(value = 1, message = "should be no less than 1")
            @get:Max(value = 10, message = "should be no greater than 10")
            @get:NotNull(message = "should be specified")
            var limitedNumber: Int?
    )
}

Validating mechanism

@Service
class ValidationService(val validator: Validator) {

    private val log = LoggerFactory.getLogger(ValidationService::class.java)

    @Throws(BodyValidationException::class)
    fun <T> validate(o: T) {
        val violations = validator.validate(o)
        if (violations.isEmpty()) return

        throw BodyValidationException.of(violations.stream()
                .map { v: ConstraintViolation<T> -> "${v.propertyPath} ${v.message}" }
                .collect(Collectors.toSet()).toMutableSet())
    }

    fun handle(e: BodyValidationException): Mono<ServerResponse> =
            handleInternal(ErrorResponse(e.errors))

    private fun handleInternal(response: ErrorResponse): Mono<ServerResponse> =
            Mono.just(response)
                    .doOnNext { res -> log.warn(res.errors.joinToString(",")) }
                    .flatMap { res ->
                        badRequest().contentType(APPLICATION_JSON)
                                .bodyValue(res)
                    }

    data class ErrorResponse(val errors: Set<String>)

    class BodyValidationException private constructor(var errors: Set<String>) : IllegalArgumentException() {
        companion object {
            fun of(errors: Set<String>): BodyValidationException {
                return BodyValidationException(errors)
            }
        }

    }
}
like image 86
Serhii Povísenko Avatar answered Nov 14 '22 22:11

Serhii Povísenko