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
I think the following solution that consist of two parts could take place for Spring Boot 2.x
:
@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?
)
}
@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)
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With