Опирается на правила:
R-VLD-OAS-1…R-VLD-OAS-6иR-VLD-OAS-X1…R-VLD-OAS-X5из Validation Style Guide → раздел 6. OpenAPI-сгенерированные DTO.
Важно знать
- OpenAPI-first. Все validation-правила входных DTO живут в YAML, не в Java. Codegen превращает их в Jakarta-аннотации автоматически.
useBeanValidation = trueв openapi-generator config — без неё generated DTO без аннотаций,@Validsilent-passes невалидные данные.- Маппинг OpenAPI keyword → Jakarta:
required→@NotNull,minLength→@Size(min=...),format: email→pattern→@Pattern,$ref→@Validна nested.- Контроллер
implements <Tag>Api(generated interface). Java-код контроллера тривиальный — все аннотации уже на параметрах generated interface.- Custom constraint, не сводимый к OpenAPI, — на wrapper-class в коде проекта. Generated DTO не правим руками.
- Двойной контракт generated vs UseCase. Generated DTO валидируется на edge через
@Valid; UseCase command (record) — не валидируется повторно. Команда пришла «уже чистой».- Дописывать
@Valid/@NotNullруками вbuild/generated/...— затрётся при следующемcompileJava/openApiGenerate.- Handcrafted
class CreateOrderRequestбез@Generated— нарушениеR-OAS-1/BS-20. Вырезать, перенести в YAML.
OpenAPI-first означает, что контракт API — это YAML. Java-код контроллеров и DTO — производная от него. Это даёт два следствия: одно место правды (изменил YAML — изменилось всё), и невозможность рассинхронизации между документацией и реализацией. Валидация — часть контракта, поэтому она тоже в YAML. Раскрытие раздела 6 гайда.
OpenAPI-first: правила в YAML
R-VLD-OAS-1: если правило выражается стандартными OpenAPI keywords — пишем в YAML, не дописываем в коде.
# src/main/resources/openapi/order-service.openapi.yaml
components:
schemas:
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'
После ./gradlew openApiGenerate появляется build/generated/.../CreateOrderRequest.java:
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", ...)
public class CreateOrderRequest {
@NotNull
@Min(1)
private Long customerId;
@Email
private String email;
@Pattern(regexp = "^\\+7\\d{10}$")
private String phone;
@NotNull
@DecimalMin("0.01")
private Double totalAmount;
@NotEmpty
@Size(min = 1, max = 100)
@Valid
private List<@Valid OrderItemRequest> items;
}
Все аннотации уже на месте. Разработчик в контроллере пишет:
@RestController
@RequiredArgsConstructor
class OrderController implements OrdersApi {
@Override
public ResponseEntity<OrderJson> createOrder(@Valid CreateOrderRequest req) {
// ...
}
}
@Valid на параметре запускает Jakarta. Все правила, описанные в YAML, проверяются. Никакого дублирования.
useBeanValidation = true обязательно
R-VLD-OAS-2: без этой опции generated DTO без аннотаций, @Valid ничего не делает.
// <module>/build.gradle.kts
openApiGenerate {
generatorName.set("spring")
inputSpec.set("$projectDir/src/main/resources/openapi/order-service.openapi.yaml")
outputDir.set("$buildDir/generated/openapi")
apiPackage.set("ru.vikulinva.order.api.gen")
modelPackage.set("ru.vikulinva.order.api.gen.dto")
configOptions.set(mapOf(
"useSpringBoot3" to "true",
"useJakartaEe" to "true",
"useBeanValidation" to "true", // ← без этого аннотаций не будет
"interfaceOnly" to "true",
"skipDefaultInterface" to "true"
))
}
Что произойдёт без флага (R-VLD-OAS-X2):
- Generated
CreateOrderRequest— POJO без@NotNull,@Size,@Email. @Validна параметре контроллера запустит Jakarta, но проверять нечего.- Невалидные данные (
{}безcustomerId) проходят на Handler. - Handler падает с
NullPointerExceptionв первой же строчке. - Клиент получает 500 вместо ожидаемого 400.
useBeanValidation = true — это обязательная настройка для всех openapi-generator конфигов в проекте, неважно spring (server-side) или spring-restclient (outbound). См. также R-RES-OAS-2 в Resilience Style Guide для outbound-клиентов.
Маппинг keyword → Jakarta annotation
R-VLD-OAS-3: знание соответствия позволяет писать правильный YAML.
| OpenAPI keyword | Java type | Jakarta annotation |
|---|---|---|
required: [field] | object property | @NotNull |
minLength: 1 | String | @Size(min=1) |
maxLength: N | String | @Size(max=N) |
pattern: '^...$' | String | @Pattern(regexp = "^...$") |
format: email | String | @Email |
format: uuid | UUID | (тип, без аннотации) |
format: date | LocalDate | (тип) |
format: date-time | OffsetDateTime | (тип) |
minimum: N | int/long | @Min(N) |
minimum: 0.01 | BigDecimal/Double | @DecimalMin("0.01") |
maximum: N | numbers | @Max(N) |
exclusiveMinimum: true + minimum: 0 | numbers | @DecimalMin(value = "0", inclusive = false) |
minItems: N | List/Set | @Size(min=N) |
maxItems: N | List/Set | @Size(max=N) |
uniqueItems: true | array | не генерируется — нужен custom validator или Set<X> тип |
enum: [A, B] | java enum | (тип) |
$ref: '#/.../X' | nested object | @Valid (рекурсивная валидация) |
Несколько комментариев к таблице:
format: uuid— генератор подставитUUIDв качестве типа. Парсинг строки в UUID происходит при десериализации Jackson — если входит мусор, ошибка ловится до Jakarta. Аннотация не нужна.minimum: 0.01наnumber— генератор выберетDoubleилиBigDecimalв зависимости отformat. Еслиformat: double—Double+@DecimalMin; если безformat— обычноBigDecimal.uniqueItems: true— спека OpenAPI 3.0 требует, но generator не имеет Jakarta-эквивалента. ИспользуемSet<X>вместоList<X>либо custom constraint.$ref— генератор автоматически расставляет@Validна nested-полях. Это сэкономило бы строчку в handcrafted DTO, но в generated происходит бесплатно.
Контроллер implements generated Api
R-VLD-OAS-4: контроллер реализует generated interface, не пишет @RestController с handcrafted маппингом.
// Generated by openapi-generator
public interface OrdersApi {
@PostMapping(value = "/orders", consumes = "application/json")
ResponseEntity<OrderJson> createOrder(
@Valid @RequestBody CreateOrderRequest createOrderRequest
);
@GetMapping(value = "/orders/{id}")
ResponseEntity<OrderJson> getOrder(
@PathVariable("id") @Min(1) Long id
);
}
// Our code
@RestController
@RequiredArgsConstructor
class OrderController implements OrdersApi {
private final UseCaseDispatcher dispatcher;
private final OrderApiMapper mapper;
@Override
public ResponseEntity<OrderJson> createOrder(CreateOrderRequest req) {
OrderId id = dispatcher.dispatch(mapper.toCommand(req));
return ResponseEntity.status(201).body(mapper.toJson(id));
}
@Override
public ResponseEntity<OrderJson> getOrder(Long id) {
Order order = dispatcher.dispatch(new GetOrder(new OrderId(id)));
return ResponseEntity.ok(mapper.toJson(order));
}
}
Что даёт:
- Все аннотации (
@Valid,@PathVariable,@Min) — на generated interface. Контроллер их «получает» через implements. - Сигнатуры всегда соответствуют OpenAPI. Меняем
CreateOrderRequestв YAML — generated interface меняется — контроллер падает на компиляции, пока не обновит метод. - Bottom-up рефактор. Если YAML говорит «POST стал PATCH» —
OrdersApiэто отразит, контроллер должен поправиться. Без generated — изменение в YAML и в коде делается двумя отдельными шагами, которые легко рассинхронизировать.
Custom constraint через wrapper-class
R-VLD-OAS-5: правило, не сводимое к OpenAPI keywords, — не дописываем в generated, а оборачиваем.
Вариант А (рекомендуется): правило выражается через pattern — пишем в OpenAPI напрямую.
# вместо custom @RussianPhone в коде —
phone:
type: string
pattern: '^\+7\d{10}$'
Generated DTO получает @Pattern(regexp = "^\\+7\\d{10}$"). Java-аннотация @RussianPhone дополнительно не нужна.
Вариант Б: правило не сводится к pattern (бизнес-проверка контрольной суммы ИНН). Применяем custom constraint на wrapper-class.
// adapter/in/rest/order/wrapper/
public record CreateOrderInput(
@Valid @RussianInn String inn,
@Valid CreateOrderRequest body
) {}
@RestController
class OrderController implements OrdersApi {
@Override
public ResponseEntity<OrderJson> createOrder(@Valid CreateOrderInput input) {
// generated CreateOrderRequest валидируется через @Valid
// дополнительный @RussianInn на inn — кастомный constraint
}
}
Generated CreateOrderRequest регенерируется без потерь; custom-логика — на wrapper, который мы пишем сами.
Двойной контракт generated vs UseCase
R-VLD-OAS-6: generated DTO валидируется. UseCase command — не валидируется повторно.
// Generated, валидируется на edge через @Valid
public class CreateOrderRequest {
@NotNull private Long customerId;
@NotEmpty @Valid private List<OrderItemRequest> items;
}
// UseCase command, в core/, БЕЗ Jakarta
public record CreateOrder(
CustomerId customerId, // ← без @NotNull
List<CreateOrderItem> items // ← без @NotEmpty
) implements UseCaseCommand {}
Почему так:
- Команда пришла из контроллера уже чистой. К моменту
dispatcher.dispatch(cmd)все Jakarta-аннотации на DTO уже сработали. Дубль на command — бесполезная работа. - core/ не зависит от Jakarta (R-MOD-2).
jakarta.validation.*вcore/order/usecase/command/— нарушение архитектуры. - Domain-инварианты — отдельный слой. «Order должен иметь хотя бы один item» проверяется в конструкторе
Orderили вOrderFactory, не Jakarta. См. Где валидировать.
Маппер из DTO в command — простой, без проверок:
@Component
class OrderApiMapper {
public CreateOrder toCommand(CreateOrderRequest req) {
return new CreateOrder(
new CustomerId(req.getCustomerId()),
req.getItems().stream().map(this::toItem).toList()
);
}
}
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Дописывать @Valid/@NotNull руками в build/generated/... | R-VLD-OAS-X1 | Правило в OpenAPI YAML; регенерация |
useBeanValidation = false или отсутствие опции | R-VLD-OAS-X2 | useBeanValidation = true обязательно |
Class-level constraint (@DateRange) на generated DTO | R-VLD-OAS-X3 | Wrapper-class в коде с class-level constraint |
| Дублирование validation в YAML и в Java | R-VLD-OAS-X4 | Один источник правды — OpenAPI YAML |
Handcrafted class CreateOrderRequest без @Generated для inbound | R-VLD-OAS-X5 | Перенести в OpenAPI, генерировать |
@RestController с handcrafted маппингом вместо implements <Tag>Api | R-VLD-OAS-4 | implements <Tag>Api из generated |
Куда дальше
- Validation → раздел 6. OpenAPI-сгенерированные DTO — нормативные формулировки
R-VLD-OAS-*. - REST API Style Guide → OpenAPI метаданные —
R-OAS-1, OpenAPI-first как принцип. - Resilience Style Guide → outbound-клиенты —
R-RES-OAS-2, тот жеuseBeanValidationдля исходящих. - Где валидировать — почему UseCase command не валидируется повторно.
- Стандартные constraints — что именно генерируется из OpenAPI keywords.
- Custom constraints — когда applicable, на wrapper-class.