Validação e mapeamento

request: o que a rota aceita

request declara o que uma rota aceita — e isso já é aplicado automaticamente pelo framework, sem nenhum step: um header ou parâmetro de query obrigatório ausente já responde 400 antes mesmo do pipeline começar a rodar.

- id: createWidget
  method: POST
  path: /widgets
  request:
    headers:
      - { name: X-Tenant-Id, required: true }
    body:
      schema: schemas/widget-create.json
      contentType: application/json

request.body.schema alimenta a documentação OpenAPI gerada, mas a validação de corpo em runtime continua sendo responsabilidade do step validate, explicitamente declarado no pipeline — as duas coisas são independentes por design.

validate: recusando o que não serve

- type: validate
  mode: collect                # collect: reporta todos os erros de uma vez; failFast: para no primeiro
  rules:
    - { expr: "$count(input.body.items) > 0", code: EMPTY_ORDER,  path: /items, message: "pedido sem itens" }
    - { expr: "input.body.total > 0",          code: INVALID_TOTAL, path: /total, message: "total inválido" }

validate aceita dois blocos, que podem ser usados juntos ou separadamente:

  • schema — um JSON Schema, valida a forma do payload (tipos, campos obrigatórios, formatos).
  • rules — uma lista de regras em JSONata, cada uma com expr (a condição que precisa ser verdadeira), code (o código de erro), path (opcional, aponta para o campo problemático) e message.

A divisão de responsabilidade é intencional: JSON Schema valida a forma, JSONata valida a regra de negócio. Misturar as duas no mesmo lugar tende a produzir schemas difíceis de ler; manter a separação deixa cada uma fazer o que faz melhor.

Uma falha em validate produz automaticamente code: validation_failed — o bloco errors: da rota (veja Referência de configuração YAML) só decide o status/title HTTP que esse código vira:

errors:
  - { code: validation_failed, status: 400, title: "Widget inválido" }

map: transformando dados

- type: map
  id: order
  to: vars.order
  expr: |
    {
      "id":         $uuid(),
      "customerId": input.body.customerId,
      "total":      $sum(input.body.items.(price * quantity)),
      "createdAt":  $now()
    }

map avalia uma expressão JSONata sobre o estado atual do pipeline e grava o resultado em to: — tipicamente um caminho sob vars. É assim que você transforma o payload recebido no formato que o resto do pipeline (persistência, resposta, evento publicado) realmente precisa.

Juntando as duas coisas

- id: createWidget
  method: POST
  path: /widgets
  request:
    headers:
      - { name: X-Tenant-Id, required: true }
  pipeline:
    - type: validate
      mode: collect
      rules:
        - { expr: "input.body.name != null", code: MISSING_NAME, path: /name, message: "name é obrigatório" }
    - type: map
      to: vars.widget
      expr: "{ 'id': $uuid(), 'name': input.body.name }"
    - type: return
  response:
    status: 201
    body: "vars.widget"
  errors:
    - { code: validation_failed, status: 400, title: "Widget inválido" }

Repare que não é preciso passar output: explícito em return quando response.body já avalia a mesma expressão — normalmente basta type: return puro e deixar response.body fazer o trabalho.

Para onde ir daqui

  • Persistência — o próximo passo natural depois de validar e mapear: gravar o dado.
  • Controle de fluxo — decidir o que fazer quando a validação falha de formas mais elaboradas do que um errors: simples permite.