Spring валидация входных DTO в Kotlin. Краткая инструкция для backend-разработчика

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

При переходе с Java на Kotlin многие вопросы приходится решать заново, а точнее по-другому. Два года назад мы начали социальный open source проект BrainUp, базируясь на Kotlin и Spring. Проект сейчас активно развивается, а мы узнаём на практике, что значит разрабатывать Kotlin-проект с нуля, какие удобства язык вносит в нашу жизнь, вместе с тем привнося свои вопросы, задачи, которые надо решать по-новому.

Например:

  1. Использование, а точнее не использование data-классов в качестве entity и почему. (напишу статью позже при возможности).

  2. Выбор code style плагина. У нас используется ktlint, инструкция настройки описана в отдельной статье.

  3. Выбор фреймворка тестирования. У нас используется Kotest.

  4. Выбор библиотеки для мокирования. У нас выбрана Mockk.

    Варианты использования Kotest и Mockk можно посмотреть у нас в проекте.

  5. Организация валидации входных DTO (Data Transfer Object) с помощью Spring.

  6. Настройка 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 = "{validation.field.fullName.empty}")
    val name: String,
    @field:NotBlank(message = "{validation.field.email.blank}")
    @field:Email(message = "{validation.field.email.invalid-format}")
    @field:Pattern(
        regexp = VALID_EMAIL_ADDRESS_REGEX_WITH_EMPTY_SPACES_ACCEPTANCE,
        message = "{validation.field.email.invalid-format.cyrillic.not.allowed}"
    )
    val email: String,
    @field:NotBlank(message = "{validation.field.password.blank}")
    @field:Size(min = 4, max = 20, message = "{validation.field.password.invalid-format}")
    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:

Stacktrace
Stacktrace

Улучшить данный вариант можно добавив читабельное сообщение message (плюс смотрите 3й шаг):

data class AudiometryHistoryRequest(
    @field:NotNull(message = "{validation.field.audiometryTaskId.notNull}")
    var audiometryTaskId: Long?,
    ...
) 

В этом случае defaultMessage будет заменён нашим, и можно будет увидеть именно определённое нами сообщение в response:

Controller response
Controller response

2 шаг. Добавление аннотации @Validated в контроллер.

Добавление аннотации @Validated в контроллер перед DTO, которую необходимо проверить при вызове данного end-point.
Например:

3 шаг. Добавление файла с сообщениями об ошибках.

Добавление файла с сообщениями об ошибках errorMessages.properties в папку resources.

На этом с валидацией всё, всем желаю удачи!

Источник: https://habr.com/ru/company/epam_systems/blog/572646/


Интересные статьи

Интересные статьи

В 80-ые интернет и цифровые миры были чем-то загадочным и мистическим, а техно-энтузиасты уже задумывались о трехмерности видеоигровых миров.  Я большой фанат видеоигр, работаю в 3D ...
Часто на практике возникает необходимость централизованной обработки исключений в рамках контроллера или даже всего приложения. В данной статье разберём основные возможности, которые пр...
Недавно на проекте интегрировал модуль CRM Битрикса c виртуальной АТС Ростелеком. Делал по стандартной инструкции, где пошагово показано, какие поля заполнять. Оказалось, следование ей не гаран...
Пару дней назад вышел релиз Spring Boot 2.3.0.M1, в описании которого первой строкой упоминается поддержка проекта Cloud Native Buildpacks, являющегося попыткой упростить жизнь разработчика, позв...
Люди едут на отдых, предвкушая тёплые пляжи, прозрачное море и отличные воспоминания. Отпуск короткий, и хочется провести его идеально, так, чтобы ничто его не омрачило. Именно поэтому разум...