Spring boot kotlin validation
When writing a web application,Often the thing to do is to perform a simple verification of the data returned by the front end,Such as whether it is non-empty and whether the character length meets the requirements,Whether the mailbox format is correct and so on.In spring boot, you can use bean validation (jsr-303) technology to perform parameter validation through annotations.
Prepare dto objects
data class userregistermodel ( @get:notempty (message="user name is required") @get:size (message="length of user name must be between 6 and 20", min=6, max=20) var username:string, @get:notempty (message="password is required") var password:string, @get:notempty (message="email is required") @get:email var email:string)
A very common dto object is defined here to carry the data when the user is registered,Maybe you have noticed something weird,Get:in front of some annotationsIt is because kotlin will directly generate getters and setters in the bytecode. This is written explicitly to indicate that this annotation should be applied to the getter.
Prepare a controller
/** * User registration interface * / @postmapping fun register (@valid @requestbody model:userregistermodel):any
Just add the @valid annotation before the parameters that need to be verified, and spring will automatically verify the correctness of the parameters.
Have a test
Parameters are complete and correct
Spring валидация входных DTO в Kotlin. Краткая инструкция для backend-разработчика
При переходе с Java на Kotlin многие вопросы приходится решать заново, а точнее по-другому. Два года назад мы начали социальный open source проект BrainUp, базируясь на Kotlin и Spring. Проект сейчас активно развивается, а мы узнаём на практике, что значит разрабатывать Kotlin-проект с нуля, какие удобства язык вносит в нашу жизнь, вместе с тем привнося свои вопросы, задачи, которые надо решать по-новому.
- Использование, а точнее не использование data-классов в качестве entity и почему. (напишу статью позже при возможности).
- Выбор code style плагина. У нас используется ktlint, инструкция настройки описана в отдельной статье.
- Выбор фреймворка тестирования. У нас используется Kotest.
- Выбор библиотеки для мокирования. У нас выбрана Mockk. Варианты использования Kotest и Mockk можно посмотреть у нас в проекте.
- Организация валидации входных DTO (Data Transfer Object) с помощью Spring.
- Настройка Sonar для Kotlin.
В этой статье расскажу про наш опыт организации валидации входных DTO с помощью Spring, с какими вопросами мы столкнулись в ходе реализации этой идеи в Kotlin и как их решали.
Итак, для добавления валидации в проект нужно пройти эти три шага:
1 шаг. Добавление аннотаций к полям в DTO
В Java мы пользовались такими аннотациями, как @NotNull, @NotEmpty, @NotBlank и др., например:
@NotNull private String userId;
Но такой вариант, переписанный на Kotlin, работать не будет:
@NotNull var userId: String
Kotlin рабочий самый простой вариант будет выглядеть так:
@field:NotNull var userId: String
Теперь рассмотрим подробнее валидации полей разных типов на реальных примерах.
1.1 Валидации для полей String работает как ожидается, вот интересные примеры из нашего проекта:
const val VALID_EMAIL_ADDRESS_REGEX_WITH_EMPTY_SPACES_ACCEPTANCE: String = "(^\\s+$)|([a-z0-9!#$%&'*+/=?^_`<|>~-]+(. [a-z0-9!#$%&'*+/=?^_`<|>~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)" data class UserAccountCreateRequest( . @field:NotEmpty(message = "") val name: String, @field:NotBlank(message = "") @field:Email(message = "") @field:Pattern( regexp = VALID_EMAIL_ADDRESS_REGEX_WITH_EMPTY_SPACES_ACCEPTANCE, message = "" ) val email: String, @field:NotBlank(message = "") @field:Size(min = 4, max = 20, message = "") var password: String, . )
1.2 Валидация для типов дат, например LocalDateTime, работает тоже как ожидается:
data class AudiometryHistoryRequest( @field:NotNull var startTime: LocalDateTime, var endTime: LocalDateTime?, . )
1.3 Валидация для типов Int, Long — тут несколько неочевидный трюк, потому что такой вариант работать не будет:
data class AudiometryHistoryRequest( @field:NotNull var audiometryTaskId: Long, . )
То есть отправляя такой json < "audiometryTaskId": null >в контроллер, мы не словим ожидаемую ошибку валидации, а увидим, что было проставлено в поле audiometryTaskId значение 0. Ищем на stackoverflow, да есть такое.
Рабочее решение выглядит несколько несуразно:
data class AudiometryHistoryRequest( @field:NotNull var audiometryTaskId: Long, . )
Здесь поле audiometryTaskId объявлено как nullable, но аннотация говорит об обратном. Для принития этого кода, необходимо иметь в голове фразу: «By making the field nullable, you’re allowing it to be constructed, so that the JSR 303 validation can run on the object. As validator doesn’t run until the object is constructed», — что означает для этих типов для валидации необходим объект, который сначала должен быть создан, т.е. сделать поля nullable для возможности создания:
var audiometryTaskId: Long?
И уже далее по созданному объекту будет произведена Spring-овая валидация, далее это значение в DTO можно спокойно использовать как не nullable:
audiometryHistoryRequest.audiometryTaskId!!
При вызове функции с audiometryTaskId=null, получим MethodArgumentNotValidException:
Улучшить данный вариант можно добавив читабельное сообщение message (смотрите 3й шаг откуда это сообщение берется):
data class AudiometryHistoryRequest( @field:NotNull(message = "") var audiometryTaskId: Long?, . )
В этом случае defaultMessage будет заменён нашим, и можно будет увидеть именно определённое нами сообщение в response:
2 шаг. Добавление аннотации @Validated в контроллер.
Добавление аннотации @Validated в контроллер перед DTO, которую необходимо проверить при вызове данного end-point.
Например:
@PostMapping @ApiOperation("Save speech audiometry history") fun save(@Validated @RequestBody audiometryHistory: AudiometryHistoryRequest): ResponseEntity = ResponseEntity.ok().body(BaseSingleObjectResponseDto(data = audiometryHistoryService.save(audiometryHistory)))
3 шаг. Добавление файла с сообщениями об ошибках (опционально).
Добавление файла с сообщениями об ошибках errorMessages.properties в папку resources, если хотите вынести сообщения в одно место.
На этом с валидацией всё, всем желаю удачи!
Spring Boot Validation in Kotlin with @field
Recently I found myself needing to validate fields in a Spring Boot controller written in Kotlin. Here’s the code I ended up with:
@Controller @RequestMapping("api/sample") class SampleController @PostMapping fun post (@Valid @RequestBody request: SampleType) return ResponseEntity.ok().build() > > data class SampleType(@field:NotBlank info: String)
The key is the @field in the data class. Without that, Kotlin will apply validation to the constructor parameters by default. This makes sense when you see it, but is less obvious when you’re wading through your 10th Java based tutorial on Spring validation.
Kotlin Spring Validation
In this article, we will learn to perform the Spring Validation in Kotlin.
2. Kotlin Spring Validation
You can use validation annotations such as @NotBlank , @NotNull with Kotlin data classes.
If you add the validation annotations as it is to the constructor parameters of the data class, it won’t work. Let’s understand why.
Consider the following User data class. We have placed the @NotBlank annotation to the userId constructor parameter.
data class User(@NotBlank @JsonProperty("userId") val userId: String?)
The Kotlin properties are compiled into a field, getter method, and if mutable ( var ) a setter method.
If you declare the val \ var property in the primary constructor, it is compiled into a constructor parameter along with a backing field and getter/setter methods.
For example, the Koltin compiler compiles the userId property in the primary constructor of the above User data class as constructor parameter and also as field:
public final data class User public constructor(@javax.validation.constraints.NotBlank @com.fasterxml.jackson.annotation.JsonProperty userId: kotlin.String?) < public final val userId: kotlin.String? /* compiled code */ public final operator fun component1(): kotlin.String? < /* compiled code */ >>
The annotation is applied only to the constructor parameter but not applied to the userId field.
To make the Spring validation work, you should explicitly mention the location to apply the annotation like @field:annotation or @get:annotation in the constructor. There’re many locations where the annotation could be placed in this context: Kotlin property, backing field, getter method, constructor parameter, setter method, or setter parameter.
The field: makes it explicit the annotation is to be applied to the backing field of the property.
For example, @field:NotBlank is applied to the userId.
data class User(@field:NotBlank @JsonProperty("userId") val userId: String)
If you check the compiled User class, the @NotBlank annotation is applied to the field as expected and thus works.
package com.tedblob.springvalidation.controllers public final data class User public constructor(@com.fasterxml.jackson.annotation.JsonProperty userId: kotlin.String?) < @field:javax.validation.constraints.NotBlank public final val userId: kotlin.String? /* compiled code */ public final operator fun component1(): kotlin.String? < /* compiled code */ >>
Let’s validate this with a real-time example. The following UserController accepts HTTP POST calls on URL /postUser . It expects a body with non-null userId .
package com.tedblob.springvalidation.controllers import com.fasterxml.jackson.annotation.JsonProperty import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import javax.validation.Valid import javax.validation.constraints.NotBlank @RestController @RequestMapping("/postUser") class UserController < @PostMapping fun postUser(@Valid @RequestBody request: User) : ResponseEntity < return ResponseEntity.ok().build() >> data class User(@field:NotBlank @JsonProperty("userId") val userId: String)
For example, the below POST call will throw a Validation failure error.
http://localhost:8080/validation BODY:
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity com.tedblob.springvalidation.controllers.ValidationController.postUser(com.tedblob.springvalidation.controllers.User): [Field error in object 'user' on field 'userId': rejected value [null]; codes [NotBlank.user.userId,NotBlank.userId,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.userId,userId]; arguments []; default message [userId]]; default message [must not be blank]] ]
3. Conclusion
To sum up, we have learned to perform Spring validation in Kotlin.