Validation Style Guide

Правила валидации входных данных Java/Spring с кодами R-VLD-*: @Valid на контроллерах и nested-DTO, @Validated на @ConfigurationProperties, custom constraints в core/validation, validation groups для Create/Update, cross-field как class-level annotation, OpenAPI useBeanValidation=true.

Статья внедрена в скилл AI-агента ucp-validation-review / ucp-validation-design

Свод правил валидации входных данных в Java/Spring-сервисах команды UCP: где валидируем, какие constraints используем, как пишем кастомные, как связано с REST API ProblemDetails и OpenAPI-генерацией. Каждое правило идентифицируется кодом (R-VLD-WHERE-1, R-VLD-OAS-X1) — скилл ucp-validation-review цитирует эти коды в findings.

Гайд опирается на Jakarta Validation 3.0 (jakarta.validation.*) и Hibernate Validator как стандартную имплементацию. Не покрывает: business-инварианты в Aggregate (это DDD: R-ENT-*/R-AGG-*), security-валидацию (CSRF, JWT — это AUTH-*), валидацию на уровне БД (CHECK constraints — это PG-T-*/PG-N-*).

Связанные стандарты:

  • R-ERR-5/R-ERR-6 — формат violations в ProblemDetails при code=VALIDATION_ERROR.
  • R-OAS-1 — OpenAPI-first контракт.
  • R-RES-OAS-2useBeanValidation=true опция openapi-generator для outbound-клиентов.
  • R-LOC-3message в violations локализуется.

Содержание

  1. Где валидируем — R-VLD-WHERE-*
  2. Стандартные constraints — R-VLD-STD-*
  3. Custom constraints — R-VLD-CC-*
  4. Validation groups — R-VLD-GRP-*
  5. Cross-field validation — R-VLD-XF-*
  6. OpenAPI-сгенерированные DTO — R-VLD-OAS-*
  7. Конфигурация — R-VLD-CFG-*
  8. Сообщения и i18n — R-VLD-MSG-*
  9. Антипаттерны — сводка R-VLD-*-X*

1. Где валидируем

В UCP-сервисе у валидации три места — каждое со своим инструментом. Перепутать = либо дублировать работу, либо проскользнут невалидные данные.

1.1 Обязательно

  • R-VLD-WHERE-1. Входной HTTP DTO на контроллере → Jakarta Validation через @Valid на параметре. Это первая линия защиты, до того как невалидные данные дойдут до handler-а.

    @PostMapping("/orders")
    public ResponseEntity<OrderJson> create(@Valid @RequestBody CreateOrderRequest req) { ... }
    

    Spring сам бросит MethodArgumentNotValidException@RestControllerAdvice маппит в 400 Bad Request с code=VALIDATION_ERROR и violations (см. R-ERR-5).

  • R-VLD-WHERE-2. @ConfigurationProperties обязательно @Validated на классе. Невалидный конфиг → BeanCreationException на старте, не «сервис поднялся, но половина флагов некорректна».

    @ConfigurationProperties("client.sber")
    @Validated
    public record SberClientSettings(
        @NotBlank String baseUrl,
        @NotNull Duration connectTimeout,
        @Min(1) int maxConcurrent
    ) {}
    
  • R-VLD-WHERE-3. Доменные инварианты Aggregate — НЕ через Jakarta. Aggregate сам гарантирует целостность через методы:

    public void confirm() {
        if (status != OrderStatus.CREATED) {
            throw new OrderDomainException("Cannot confirm: status=" + status);
        }
        if (items.isEmpty()) {
            throw new OrderDomainException("Cannot confirm: empty order");
        }
        this.status = OrderStatus.CONFIRMED;
        this.confirmedAt = OffsetDateTime.now();
    }
    

    Бросает domain-specific exception, который @RestControllerAdvice маппит в 409 Conflict или 400 Bad Request с конкретным code (например ORDER_EMPTY, ORDER_ALREADY_CONFIRMED). См. также R-ENT-*/R-AGG-* в DDD style guide.

  • R-VLD-WHERE-4. При наличии nested DTO (CreateOrderRequest содержит List<OrderItemRequest>) — на nested-поле обязательно @Valid:

    public record CreateOrderRequest(
        @NotNull Long customerId,
        @NotEmpty @Valid List<OrderItemRequest> items     // @Valid обязателен — иначе nested не валидируется
    ) {}
    

1.2 Запрещено

  • R-VLD-WHERE-X1. Manual if (cmd.amount() < 0) throw ... в Handler для входной валидации. Теряется единый формат violations в ProblemDetails. Если правило про входной DTO — @Valid на контроллере; если про доменный инвариант — метод агрегата.

  • R-VLD-WHERE-X2. Дублирование Jakarta-валидации на UseCase command (record). Контроллер уже провалидировал входной DTO; перенос в <X>UseCase record → @Validated на handler-е = двойная работа без пользы.

  • R-VLD-WHERE-X3. @ConfigurationProperties без @Validated. Сервис стартует с невалидным конфигом, падает на первом запросе с непонятной ошибкой.

  • R-VLD-WHERE-X4. Доменный инвариант через Jakarta-аннотацию на Aggregate-поле (@Min(1) на поле quantity в OrderItem). Aggregate-поля иммутабельны после конструирования; инвариант проверяется в конструкторе/методе бросанием domain exception.


2. Стандартные constraints

Используем стандартный набор Jakarta. Не изобретаем замену для существующих.

2.1 Обязательно

  • R-VLD-STD-1. Базовые null/empty проверки:

    • @NotNull — для object-полей и Boolean.
    • @NotBlank — для строк (одна аннотация вместо @NotNull + @NotEmpty + проверка пробелов).
    • @NotEmpty — для коллекций / массивов / Map.
  • R-VLD-STD-2. Размеры:

    • @Size(min, max) — для строк (тогда — без @NotBlank, потому что @Size(min=1) это фактически not-empty) и коллекций.
    • @Min / @Max — для int / long / short / byte.
    • @DecimalMin / @DecimalMax — для BigDecimal / BigInteger. Не использовать @Min на BigDecimal (только примитивы).
    • @Positive / @PositiveOrZero / @Negative / @NegativeOrZero — короткая форма для знака.
  • R-VLD-STD-3. Формат:

    • @Email — для email-адресов. Использовать regex @Pattern("^[^@]+@[^@]+$") запрещено — Jakarta @Email корректнее, обновляется при изменениях RFC.
    • @Pattern(regexp) — только для редких форматов (артикул [A-Z]{3}-\d{6}). Для частых форматов (телефон E.164, INN, BIC) — custom constraint (см. R-VLD-CC-*).
  • R-VLD-STD-4. Время:

    • @Past / @PastOrPresent / @Future / @FutureOrPresent — для LocalDate/Instant/OffsetDateTime.
  • R-VLD-STD-5. Тип-зависимая валидация:

    • Для boolean-поля, которое может быть null — тип Boolean (не boolean) + @NotNull.
    • Для int-поля, которое не может быть пустым — примитив int (не Integer); ноль — валидное значение, отдельная аннотация не нужна.

2.2 Запрещено

  • R-VLD-STD-X1. @NotNull на примитивах (@NotNull int amount). Примитив не может быть null. Аннотация молчaливо ничего не проверяет, создаёт ложную гарантию.

  • R-VLD-STD-X2. Кастомный regex в @Pattern для форматов, у которых уже есть стандартная аннотация: @Pattern("^[^@]+@[^@]+$") вместо @Email. Хуже валидирует и тяжелее читается.

  • R-VLD-STD-X3. Composite-аннотации проекта (@NotBlankAndAtMost50) поверх стандартных. Лучше две отдельные на поле — компилятор их легко прочитает.


3. Custom constraints

Кастомные constraints — для доменных правил, которых нет в стандартной Jakarta.

3.1 Обязательно

  • R-VLD-CC-1. Custom constraint оформляется как пара: annotation interface + ConstraintValidator implementation.

    // common/validation/RussianPhone.java
    @Target({ FIELD, PARAMETER })
    @Retention(RUNTIME)
    @Constraint(validatedBy = RussianPhoneValidator.class)
    public @interface RussianPhone {
        String message() default "Номер должен быть в формате +7XXXXXXXXXX";
        Class<?>[] groups() default {};
        Class<? extends Payload>[] payload() default {};
    }
    
    // common/validation/RussianPhoneValidator.java
    public class RussianPhoneValidator implements ConstraintValidator<RussianPhone, String> {
        private static final Pattern PHONE = Pattern.compile("^\\+7\\d{10}$");
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext ctx) {
            return value == null || PHONE.matcher(value).matches();   // null — валидно (комбинируется с @NotBlank)
        }
    }
    
  • R-VLD-CC-2. Расположение кастомных constraints:

    • Доменно-специфичный (@VatNumber, @Iso8601Duration) → core/<bc>/validation/ (часть domain-vocabulary).
    • Общий технический (@RussianPhone, @UrlSafeBase64) → common/validation/ отдельный модуль.
  • R-VLD-CC-3. Имена: @<DomainTerm> без префиксов Valid/Check/Is. @RussianPhone, @VatNumber — да; @ValidPhone, @CheckVat — нет.

  • R-VLD-CC-4. isValid(null, ...) возвращает true — null обрабатывается отдельной @NotNull. Custom-constraint обязан комбинироваться с null-аннотацией.

    @NotBlank @RussianPhone String phone     // null/blank — @NotBlank; формат — @RussianPhone
    
  • R-VLD-CC-5. ConstraintValidator — stateless, без @Autowired-полей с runtime-state. Если нужны зависимости (DI на справочник) — использовать HibernatePropertyNodeBuilderCustomizable или явно initialize(annotation) с обращением к Validator-context. Но в большинстве случаев validator — pure function от value.

3.2 Запрещено

  • R-VLD-CC-X1. isValid(null, ...) возвращает false. Нарушает композицию с @NotNull/@NotBlank — если ставишь обе, первая бесполезна.

  • R-VLD-CC-X2. Custom constraint в одном файле с DTO (как inner-аннотация). Не переиспользуется, не находится grep-ом.

  • R-VLD-CC-X3. Constraint-логика inline в @AssertTrue-методе на DTO. Не переиспользуется.


4. Validation groups

Validation groups — механизм «один класс, разные правила в разных контекстах». Использовать узко.

4.1 Обязательно

  • R-VLD-GRP-1. Validation groups применяй только когда тот же класс DTO нужен в разных сценариях с разными required-полями. Типичный кейс — OrderRequest для Create и Update:

    public interface OnCreate {}
    public interface OnUpdate {}
    
    public record OrderRequest(
        @NotNull(groups = OnCreate.class) Long customerId,    // required only при создании
        @NotNull Money totalAmount,                            // required всегда
        @Size(max = 1000) String comment
    ) {}
    
    // controller
    @PostMapping
    public Order create(@Validated(OnCreate.class) @RequestBody OrderRequest req) { ... }
    
    @PatchMapping
    public Order update(@Validated(OnUpdate.class) @RequestBody OrderRequest req) { ... }
    
  • R-VLD-GRP-2. Group-interface — пустой interface с doc-comment «применяется в <контексте>». Не extends Default, не имеет методов.

4.2 Запрещено

  • R-VLD-GRP-X1. Группы для разделения «строгая / мягкая валидация». Это два разных DTO в духе CreateOrderRequest vs DraftOrderRequest, а не один с группами.

  • R-VLD-GRP-X2. Цепочки групп @Validated({OnCreate.class, OnConfirm.class, OnPay.class}). Если правил для одного класса больше двух режимов — это запах «класс делает слишком много», разбивай.


5. Cross-field validation

5.1 Обязательно

  • R-VLD-XF-1. Cross-field constraint (правило, в котором участвуют 2+ поля одного объекта) — class-level annotation:

    @Target({ TYPE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = DateRangeValidator.class)
    public @interface DateRange {
        String message() default "dateFrom должен быть не позже dateTo";
        Class<?>[] groups() default {};
        Class<? extends Payload>[] payload() default {};
    }
    
    public class DateRangeValidator implements ConstraintValidator<DateRange, OrderFilterRequest> {
        @Override
        public boolean isValid(OrderFilterRequest req, ConstraintValidatorContext ctx) {
            if (req.dateFrom() == null || req.dateTo() == null) return true;
            if (!req.dateFrom().isAfter(req.dateTo())) return true;
            ctx.disableDefaultConstraintViolation();
            ctx.buildConstraintViolationWithTemplate(ctx.getDefaultConstraintMessageTemplate())
               .addPropertyNode("dateFrom")
               .addConstraintViolation();
            return false;
        }
    }
    
    @DateRange
    public record OrderFilterRequest(
        LocalDate dateFrom, LocalDate dateTo, ...
    ) {}
    

    В violations ошибка прицепится к field: "dateFrom" (или конкретному полю, как настроено), не на всему объекту.

  • R-VLD-XF-2. Имя cross-field-constraint описывает правило, не объект: @DateRange, @PasswordsMatch, @AmountWithinLimit — да; @OrderRequestValid — нет (что валидируется?).

5.2 Запрещено

  • R-VLD-XF-X1. @AssertTrue-метод в DTO (@AssertTrue boolean isDateRangeValid()). Не переиспользуется в другие DTO с тем же правилом, теряется при рефакторинге.

  • R-VLD-XF-X2. Cross-field валидация в Handler перед dispatcher.dispatch(...). Это валидация контракта, а не бизнес-правило — должна быть на DTO-уровне.


6. OpenAPI-сгенерированные DTO

Связка с REST API style guide: контроллеры implements generated <Tag>Api, DTO генерируются openapi-generator. Принцип в команде — OpenAPI-first для валидации: все правила входных DTO формулируются в OpenAPI YAML, codegen превращает их в Jakarta-аннотации автоматически. Java-код только подключает @Valid на параметре контроллера и (при необходимости) добавляет custom constraints, которых нет в OpenAPI.

Workflow для нового эндпоинта:

ucp-api-design → src/main/resources/openapi/<service>.openapi.yaml
                  ↓ ./gradlew openApiGenerate
              build/generated/.../*Request.java   ← с Jakarta-аннотациями
                  ↓ controller implements <Tag>Api + @Valid
              automatic 400 + violations при невалидных данных

6.1 Обязательно

  • R-VLD-OAS-1. OpenAPI-first. Все validation-правила для входных DTO живут в OpenAPI YAML, не в Java-коде. Если правило выражается через standard OpenAPI keywords — пиши в YAML, не дописывай аннотации в коде. В Java добавляются только custom constraints, которых OpenAPI не покрывает (см. R-VLD-OAS-5).

  • R-VLD-OAS-2. Опция useBeanValidation = true в openapi-generator config. Без неё generated DTO без аннотаций, @Valid на контроллере ничего не делает.

    // <module>/build.gradle.kts
    openApiGenerate {
        generatorName.set("spring")          // или spring-restclient для outbound
        configOptions.set(mapOf(
            "useSpringBoot3" to "true",
            "useJakartaEe" to "true",
            "useBeanValidation" to "true"     // ← обязательно
        ))
    }
    
  • R-VLD-OAS-3. OpenAPI YAML формулирует constraints на уровне схемы. Соответствие keyword → Java type → Jakarta annotation:

    OpenAPI keywordJava typeJakarta annotation
    required: [field]object property@NotNull (для object/Long), без @NotBlank для строк
    minLength: 1String@Size(min=1) (фактически not-empty)
    maxLength: NString@Size(max=N)
    minLength: 1, maxLength: NString@Size(min=1, max=N)
    pattern: '^...$'String@Pattern(regexp = "^...$")
    format: emailString@Email
    format: uuidUUID(тип, не аннотация)
    format: dateLocalDate(тип)
    format: date-timeOffsetDateTime(тип)
    minimum: Nint/long@Min(N)
    minimum: 0.01BigDecimal@DecimalMin("0.01")
    maximum: Nint/long@Max(N)
    exclusiveMinimum: true + minimum: 0numbers@DecimalMin(value = "0", inclusive = false)
    minItems: NList/Set@Size(min=N)
    maxItems: NList/Set@Size(max=N)
    uniqueItems: truearrayне генерируется — нужен custom validator или Set<X> тип
    enum: [A, B]java enum(тип, не аннотация)
    $ref: '#/.../X'nested object@Valid (рекурсивная валидация)

    Пример входного DTO в YAML:

    CreateOrderRequest:
      type: object
      required:
        - customerId
        - items
      properties:
        customerId:
          type: integer
          format: int64
          minimum: 1
        email:
          type: string
          format: email
        phone:
          type: string
          pattern: '^\+7\d{10}$'
        totalAmount:
          type: number
          format: double
          minimum: 0.01
        items:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/OrderItemRequest'
    

    Generated CreateOrderRequest.java уже содержит: @NotNull, @Min(1), @Email, @Pattern("^\\+7\\d{10}$"), @DecimalMin("0.01"), @NotEmpty, @Size(min=1, max=100), @Valid на nested.

    Контроллер тривиален:

    @PostMapping("/orders")
    public ResponseEntity<OrderJson> create(@Valid @RequestBody CreateOrderRequest req) { ... }
    
  • R-VLD-OAS-4. Контроллер обязательно implements <Tag>Api (generated interface), не @RestController с handcrafted маппингом. Это гарантирует что generated @Valid/Jakarta-аннотации применяются (см. R-OAS-1).

    @RestController
    public class OrderController implements OrdersApi {
        @Override
        public ResponseEntity<OrderJson> createOrder(CreateOrderRequest req) { ... }
        // generated OrdersApi method signature уже имеет @Valid внутри (благодаря useBeanValidation)
    }
    
  • R-VLD-OAS-5. Custom constraint, который не выражается standard OpenAPI keywords:

    Вариант А (рекомендуется): custom формат уже выражается через pattern: '^...$' — пиши в OpenAPI напрямую. Например @RussianPhone = pattern: '^\+7\d{10}$'. Дублирование java-аннотации не нужно.

    Вариант Б: правило не сводится к pattern (например бизнес-проверка ИНН с контрольной суммой). Применяй на wrapper-class в коде проекта:

    // адаптер в *-in-adapter
    public record CreateOrderInput(@Valid @RussianInn String inn, @Valid CreateOrderRequest body) {}
    
    @PostMapping
    public ResponseEntity<OrderJson> create(@Valid @RequestBody CreateOrderInput input) { ... }
    

    Generated CreateOrderRequest остаётся регенерируемым; custom-логика — на wrapper.

    Вариант В (если генератор поддерживает): OpenAPI extension x-validation:

    inn:
      type: string
      x-validation: russianInn      # генератор-специфичный extension
    

    Поддержка зависит от templates генератора — обычно нужно кастомизировать mustache-шаблон. Не используй без явного решения в команде; дефолт — Вариант А или Б.

  • R-VLD-OAS-6. Двойной контракт generated vs UseCase. Generated DTO (CreateOrderRequest) валидируется через @Valid на контроллере. После маппинга в UseCase command (CreateOrderCommand record) — повторная валидация не делается. Команда пришла «уже чистой» из контроллера. Domain-инварианты (R-VLD-WHERE-3) — отдельный концерн на агрегате, не Jakarta.

6.2 Запрещено

  • R-VLD-OAS-X1. Дописывать @Valid/@NotNull/@Pattern руками в generated DTO (build/generated/.../CreateOrderRequest.java) — затрётся при следующем compileJava/openApiGenerate.

  • R-VLD-OAS-X2. useBeanValidation = false или отсутствие этой опции. Generated DTO без constraints; @Valid на контроллере silent-passes невалидные данные в Handler.

  • R-VLD-OAS-X3. Class-level constraint (@DateRange) на generated DTO. Применяй на wrapper-class или формулируй cross-field правила через OpenAPI bool-логику (если возможно).

  • R-VLD-OAS-X4. Дублирование validation-правил: то же самое в OpenAPI YAML и руками в коде. Источник правды один — OpenAPI YAML. Если правило только в коде — оно не отразится в OpenAPI-документации, и фронт/потребители не узнают про ограничение.

  • R-VLD-OAS-X5. Handcrafted DTO в jsonbean/ или подобном пакете для inbound REST API. Это нарушение R-OAS-1/BS-20 — все request/response DTO генерируются из OpenAPI. Если видишь class CreateOrderRequest без @Generated — вырезать, перенести в YAML.


7. Конфигурация

@ConfigurationProperties + @Validated — стандартный паттерн UCP.

7.1 Обязательно

  • R-VLD-CFG-1. Каждый @ConfigurationProperties класс имеет @Validated на классе. Невалидный конфиг → fail-fast на старте.

  • R-VLD-CFG-2. Required-поля помечены @NotNull (для object-типов) или @NotBlank (для String):

    @ConfigurationProperties("client.sber")
    @Validated
    public record SberClientSettings(
        @NotBlank String baseUrl,
        @NotNull Duration connectTimeout,
        @NotNull Duration readTimeout,
        @Min(1) @Max(100) int maxConcurrent,
        String apiKey                              // optional, без аннотации = nullable
    ) {}
    
  • R-VLD-CFG-3. Spring валидирует Duration / DataSize по типу. Дополнительные @DurationMin/@DurationMax — только если нужен бизнес-предел (@DurationMax(value = 60, unit = SECONDS)).

  • R-VLD-CFG-4. Если property — структура (nested), используй @Valid для рекурсивной валидации:

    @Validated
    public record AppSettings(
        @Valid @NotNull DatabaseSettings database,
        @Valid @NotNull MessagingSettings messaging
    ) {}
    

7.2 Запрещено

  • R-VLD-CFG-X1. @ConfigurationProperties без @Validated (см. R-VLD-WHERE-X3).

  • R-VLD-CFG-X2. @Value("${prop}") для required-конфига. @Value не валидируется; используй @ConfigurationProperties (typed + validated) даже для одного поля.


8. Сообщения и i18n

8.1 Обязательно

  • R-VLD-MSG-1. message в аннотации — на русском, для пользователя (см. R-LOC-3).

    @NotBlank(message = "Имя обязательно")
    @Size(max = 100, message = "Имя не более 100 символов")
    String name
    
  • R-VLD-MSG-2. Интерполяция значений — через {}-плейсхолдеры из спецификации:

    @Min(value = 1, message = "Значение должно быть не меньше {value}")
    @Size(min = 1, max = 100, message = "Длина от {min} до {max}")
    
  • R-VLD-MSG-3. Если нужна i18n — message-bundle через {key}:

    @NotBlank(message = "{order.name.required}")
    String name
    

    В messages_ru.properties:

    order.name.required=Имя заказа обязательно
    

8.2 Запрещено

  • R-VLD-MSG-X1. Английский в message для пользовательских правил. Будет в violations.message → пользователю на UI.

  • R-VLD-MSG-X2. Технические термины в message: «Field amount must be positive» → «Сумма должна быть положительной». Сообщение читает обычный пользователь, не разработчик.

  • R-VLD-MSG-X3. Дублирование message в каждом DTO для одного и того же constraint. Если @RussianPhone имеет default message — не переопределяй на каждом поле без бизнес-причины.


9. Антипаттерны

АнтипаттернПравилоКорректно
Manual if (cmd.x < 0) throw в Handler для входной валидацииR-VLD-WHERE-X1@Valid на контроллере
Дублирование Jakarta на UseCase commandR-VLD-WHERE-X2один раз на контроллере
@ConfigurationProperties без @ValidatedR-VLD-WHERE-X3, R-VLD-CFG-X1@Validated на классе
Доменный инвариант через @Min на поле AggregateR-VLD-WHERE-X4бросание domain exception в методе
@NotNull на примитивеR-VLD-STD-X1проверка не нужна, либо тип Long/Integer
Кастомный regex для email вместо @EmailR-VLD-STD-X2@Email
Composite-аннотация @NotBlankAndAtMost50R-VLD-STD-X3две отдельные аннотации
Custom validator возвращает false для nullR-VLD-CC-X1isValid(null) → true + комбинация с @NotNull
Inner-аннотация в одном файле с DTOR-VLD-CC-X2в core/<bc>/validation/ или common/validation/
@AssertTrue isDateRangeValid() метод в DTOR-VLD-XF-X1, R-VLD-CC-X3class-level @DateRange
Cross-field валидация в HandlerR-VLD-XF-X2class-level constraint на DTO
Validation groups «строгая/мягкая» вместо разных DTOR-VLD-GRP-X1разные DTO
Цепочки @Validated({OnCreate, OnConfirm, OnPay})R-VLD-GRP-X2разбить класс
Дописывание @Valid/@NotNull в generated DTOR-VLD-OAS-X1constraints в OpenAPI YAML
useBeanValidation = falseR-VLD-OAS-X2true обязательно
Class-level constraint на generated DTOR-VLD-OAS-X3wrapper-class в коде проекта
Дублирование validation в YAML и в JavaR-VLD-OAS-X4OpenAPI YAML — единственный источник
Handcrafted request DTO без OpenAPIR-VLD-OAS-X5сгенерировать из YAML
@Value("${prop}") для required-конфигаR-VLD-CFG-X2@ConfigurationProperties typed
Английский в messageR-VLD-MSG-X1русский
Технические термины в messageR-VLD-MSG-X2пользовательский язык
@Valid забыт на nested-DTOR-VLD-WHERE-4@Valid на nested-поле

Финальная сводка: правил «Обязательно» — около 25, «Запрещено» — около 18.