Опирается на правила: 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,uuid4required: [customer_id] + format: uuid; max=1000maxLength: 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-X1x-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-X5Typed struct с validate-тегами
validate.Struct(cmd) после маппинга из чистого DTOR-VLD-OAS-6Команда уже чистая; повторная валидация не нужна
Правило валидации в двух местах: struct-тег + комментарий в YAML без связиR-VLD-OAS-X4Code-first: тег — истина; YAML обновляется рядом

Куда дальше

  • Validation → раздел 6. Контракт-схема — нормативные формулировки R-VLD-OAS-*.
  • Где валидировать — httpreq.Decode как точка применения struct-тегов.
  • Custom constraints — кастомные теги для форматов, не покрытых OpenAPI.
  • Validation groups — разные struct для разных операций (create vs update).