Опирается на правила:
R-VLD-OAS-1,R-VLD-OAS-4,R-VLD-OAS-6иR-VLD-OAS-X1,R-VLD-OAS-X4,R-VLD-OAS-X5из Validation Style Guide → раздел 6. Контракт-схема.
Важно знать
- В Go UCP-проекте подход code-first: struct-теги — источник правды; OpenAPI генерируется или обновляется рядом.
- Правило валидации живёт в одном месте — в struct-теге
validate:"...". Не в YAML и не дублируется в код.- При contract-first (
oapi-codegenиз YAML): validate-теги задаются черезx-oapi-codegen-extra-tagsв YAML, не дописываются вручную в сгенерированный файл.- После маппинга DTO в UseCase-команду повторная валидация не делается — команда «уже чистая».
- Inbound данные как
map[string]anyилиjson.RawMessageбез декодирования в typed struct — нарушение контракта.toCommand(req)— pure mapping безvalidate.Struct; логика только в присвоениях.
В Go нет OpenAPI-генератора по типу openapi-generator с useBeanValidation, но принцип «один источник правды» соблюдается через struct-теги: меняешь тег — меняется и спека, и поведение валидации одновременно.
Code-first: struct-теги как контракт
R-VLD-OAS-1: в code-first Go-проекте struct-теги — единственное место, где живёт правило валидации. OpenAPI-спека обновляется при изменении struct.
// edge/order/request.go
type CreateOrderRequest struct {
CustomerID string `json:"customer_id" validate:"required,uuid4"`
Items []OrderItemRequest `json:"items" validate:"required,min=1,dive"`
Comment string `json:"comment" validate:"max=1000"`
PromoCode *string `json:"promo_code" validate:"omitempty,min=4,max=16"`
}
type OrderItemRequest struct {
ProductID string `json:"product_id" validate:"required,uuid4"`
Qty int `json:"qty" validate:"required,min=1,max=999"`
}
Соответствующая OpenAPI-схема генерируется или поддерживается вручную:
# openapi/order.yaml
components:
schemas:
CreateOrderRequest:
type: object
required: [customer_id, items]
properties:
customer_id:
type: string
format: uuid
items:
type: array
minItems: 1
items:
$ref: '#/components/schemas/OrderItemRequest'
comment:
type: string
maxLength: 1000
promo_code:
type: string
minLength: 4
maxLength: 16
Правила required,uuid4 → required: [customer_id] + format: uuid; max=1000 → maxLength: 1000. Один-к-одному.
Contract-first: oapi-codegen + x-oapi-codegen-extra-tags
R-VLD-OAS-4: при contract-first (YAML → Go код через oapi-codegen) validate-теги задаются в YAML-расширении, не дописываются вручную:
# openapi/order.yaml
components:
schemas:
CreateOrderRequest:
type: object
required: [customer_id, items]
properties:
customer_id:
type: string
format: uuid
x-oapi-codegen-extra-tags:
validate: "required,uuid4"
items:
type: array
minItems: 1
x-oapi-codegen-extra-tags:
validate: "required,min=1,dive"
items:
$ref: '#/components/schemas/OrderItemRequest'
comment:
type: string
maxLength: 1000
x-oapi-codegen-extra-tags:
validate: "max=1000"
Сгенерированный struct получит validate-теги автоматически. Ручная правка gen/ директории — R-VLD-OAS-X1: затрётся при следующей регенерации.
Конфиг oapi-codegen.yaml:
package: gen
generate:
models: true
chi-server: true
output: gen/server.gen.go
Маппинг в UseCase-команду — чистая функция
R-VLD-OAS-6: после валидации DTO команда конструируется из чистых данных без повторной валидации.
// edge/order/mapper.go
func toCreateOrderCommand(req CreateOrderRequest) order.CreateCommand {
return order.CreateCommand{
CustomerID: order.CustomerID(req.CustomerID),
Items: toOrderItems(req.Items),
Comment: req.Comment,
PromoCode: req.PromoCode,
}
}
func toOrderItems(items []OrderItemRequest) []order.Item {
result := make([]order.Item, len(items))
for i, item := range items {
result[i] = order.Item{
ProductID: order.ProductID(item.ProductID),
Qty: item.Qty,
}
}
return result
}
Нет validate.Struct(cmd) — команда собирается из уже чистого DTO. Нет if cmd.CustomerID == "" — это было проверено на границе. Маппер — pure transformation.
Handler:
func (h *OrderHandler) Create(w http.ResponseWriter, r *http.Request) {
req, err := httpreq.Decode[CreateOrderRequest](r)
if err != nil {
httperr.Write(w, r, err)
return
}
cmd := toCreateOrderCommand(req) // pure mapping
result, err := h.uc.Handle(r.Context(), cmd)
if err != nil {
httperr.Write(w, r, err)
return
}
render.JSON(w, http.StatusCreated, result)
}
Inbound map[string]any — нет
R-VLD-OAS-X5: map[string]any как inbound-структура — потеря типизации, невозможность валидации через struct-теги.
// ПЛОХО — контракт не выражен
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
var body map[string]any
json.NewDecoder(r.Body).Decode(&body)
customerID, ok := body["customer_id"].(string)
if !ok || customerID == "" {
http.Error(w, "customer_id required", 400)
return
}
// ручные if-проверки на каждое поле...
}
// ХОРОШО — typed struct + validate
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
req, err := httpreq.Decode[CreateOrderRequest](r)
if err != nil {
httperr.Write(w, r, err)
return
}
// ...
}
map[string]any лишает возможности использовать validate.Struct, не даёт typed доступа к полям, не генерирует OpenAPI-схему. Typed struct — единственный допустимый формат inbound DTO.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Ручная правка validate-тегов в сгенерированных файлах gen/ | R-VLD-OAS-X1 | x-oapi-codegen-extra-tags в YAML |
validate.Struct(req) и дополнительно if req.Amount <= 0 на то же поле | R-VLD-OAS-X4 | Один источник правды — struct-тег gt=0 |
map[string]any / json.RawMessage как inbound-тип | R-VLD-OAS-X5 | Typed struct с validate-тегами |
validate.Struct(cmd) после маппинга из чистого DTO | R-VLD-OAS-6 | Команда уже чистая; повторная валидация не нужна |
| Правило валидации в двух местах: struct-тег + комментарий в YAML без связи | R-VLD-OAS-X4 | Code-first: тег — истина; YAML обновляется рядом |
Куда дальше
- Validation → раздел 6. Контракт-схема — нормативные формулировки
R-VLD-OAS-*. - Где валидировать —
httpreq.Decodeкак точка применения struct-тегов. - Custom constraints — кастомные теги для форматов, не покрытых OpenAPI.
- Validation groups — разные struct для разных операций (create vs update).