---
title: "OpenAPI em Zig: Contratos, JSON e Clientes sem Framework Pesado"
url: "https://ziglang.com.br/artigos/zig-openapi-contratos-json-clientes/"
markdown_url: "https://ziglang.com.br/artigos/zig-openapi-contratos-json-clientes.MD"
description: "Como usar OpenAPI em projetos Zig para alinhar contratos HTTP, validar JSON, gerar clientes simples, testar compatibilidade e evitar drift em APIs REST."
date: "2026-06-01"
author: ""
---

# OpenAPI em Zig: Contratos, JSON e Clientes sem Framework Pesado

Como usar OpenAPI em projetos Zig para alinhar contratos HTTP, validar JSON, gerar clientes simples, testar compatibilidade e evitar drift em APIs REST.


OpenAPI não precisa significar um framework enorme ou um gerador mágico que toma conta do projeto. Em Zig, ele funciona melhor como contrato de fronteira: um arquivo versionado que descreve rotas, métodos, corpos JSON, códigos de resposta e regras mínimas que clientes e servidores devem respeitar. O serviço continua pequeno, explícito e compilado como sempre. O contrato só evita que a API vire uma coleção de decisões escondidas no código.

Isso é especialmente útil quando um binário Zig conversa com front-end, aplicativos móveis, CLIs internas, workers, integrações de parceiros ou serviços escritos em Go, Python, Kotlin e Rust. Sem um contrato claro, cada mudança de campo vira uma aposta: alguém lembra de atualizar o cliente? A resposta `404` tem o mesmo formato da `422`? O campo opcional aceita `null` ou simplesmente não aparece? O endpoint novo está documentado no README ou só no handler?

Este guia mostra uma forma prática de usar **OpenAPI em Zig** sem perder a filosofia da linguagem: contrato simples, tipos explícitos, validação no limite da aplicação, testes de compatibilidade e geração de clientes apenas onde ela reduz trabalho real. Para aprofundar as peças citadas aqui, leia também [API REST em Zig](/artigos/zig-api-rest-completa/), [parsing JSON em Zig](/tutoriais/parsing-json-zig/), [validação de JSON](/receitas/zig-json-validar/), [HTTP client em Zig](/tutoriais/zig-http-client/) e [testes em Zig](/tutoriais/testes-zig/).

## Quando OpenAPI vale o custo

Se a API tem um único consumidor dentro do mesmo repositório, um teste de integração bem escrito talvez seja suficiente. OpenAPI começa a pagar quando existe mais de uma fronteira humana ou técnica.

Use um contrato quando você tem:

- front-end consumindo endpoints Zig;
- CLI ou SDK interno que chama a API;
- outro time ou agente gerando integrações;
- webhooks recebidos ou enviados;
- versionamento público de rotas;
- testes de regressão para payloads JSON;
- documentação que precisa sobreviver ao autor original;
- mudança frequente em campos de request e response.

Evite usar OpenAPI como desculpa para projetar uma plataforma antes da hora. O arquivo deve cobrir a superfície pública real. Se você ainda está explorando nomes de campos em um protótipo local, comece com testes e exemplos. Quando a rota vira contrato entre processos, promova para OpenAPI.

## O papel certo do contrato em Zig

OpenAPI não substitui o código Zig. Ele descreve a promessa externa. O handler ainda precisa fazer parsing, checar limites, validar regras de negócio e mapear erros.

Uma divisão saudável fica assim:

| Camada | Responsabilidade |
|---|---|
| `openapi.yaml` | métodos, caminhos, schemas, exemplos, códigos de resposta |
| tipos Zig | representação interna usada pelo serviço |
| parser JSON | transformar bytes em valores com limite de memória |
| validação | regras que o schema não expressa bem |
| testes | provar que exemplos e handlers continuam compatíveis |
| cliente gerado/manual | reduzir repetição do lado consumidor |

O erro comum é tentar gerar todo o servidor a partir do contrato. Isso costuma produzir abstrações piores que o próprio problema: roteadores pouco idiomáticos, tipos grandes demais, alocação invisível e mensagens de erro genéricas. Em Zig, prefira o inverso: escreva o serviço explicitamente e use OpenAPI como artefato de compatibilidade.

## Comece pequeno com um `openapi.yaml`

Um contrato inicial pode caber em um arquivo curto. Ele não precisa listar todos os detalhes internos, mas deve ser preciso nos nomes e formatos que cruzam a rede.

```yaml
openapi: 3.1.0
info:
  title: Exemplo Zig API
  version: 1.0.0
paths:
  /v1/jobs:
    post:
      summary: Cria uma tarefa em background
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateJobRequest'
      responses:
        '202':
          description: Tarefa aceita
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobAccepted'
        '422':
          description: Payload inválido
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
components:
  schemas:
    CreateJobRequest:
      type: object
      required: [kind, payload]
      additionalProperties: false
      properties:
        kind:
          type: string
          enum: [relatorio, webhook, compactacao]
        payload:
          type: object
    JobAccepted:
      type: object
      required: [id, status]
      properties:
        id:
          type: string
        status:
          type: string
          enum: [queued]
    ApiError:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
        message:
          type: string
```

Esse arquivo já responde perguntas importantes: a rota é `POST`, aceita JSON, retorna `202` em caso de sucesso e usa um formato padronizado para erro de validação. O `additionalProperties: false` também força uma decisão: campos desconhecidos são rejeitados ou ignorados?

## Mapeando schema para tipos Zig

Não tente representar cada detalhe de JSON Schema no tipo Zig. Use o tipo para o que o serviço realmente consome e deixe validações de contrato nos testes ou na borda.

```zig
const std = @import("std");

const JobKind = enum {
    relatorio,
    webhook,
    compactacao,
};

const CreateJobRequest = struct {
    kind: JobKind,
    payload: std.json.Value,
};

const JobAccepted = struct {
    id: []const u8,
    status: []const u8 = "queued",
};
```

Esse desenho deixa duas coisas explícitas. Primeiro, `kind` virou `enum`, então valores desconhecidos falham cedo. Segundo, `payload` continua como `std.json.Value` porque cada tipo de job pode ter regra própria. Se você tentar modelar todos os payloads em uma única struct, a API pode ficar rígida demais antes de o domínio estabilizar.

Para requests simples, uma struct completa é melhor. Para payloads heterogêneos, valide por etapas: envelope primeiro, regra específica depois.

## Parsing com limite de memória

Contrato nenhum protege o processo se você aceitar qualquer corpo de request. Antes de chamar `std.json.parseFromSlice`, imponha limite de bytes no servidor HTTP. Depois, parseie com allocator controlado e libere tudo no fim da requisição.

```zig
fn parseCreateJob(allocator: std.mem.Allocator, body: []const u8) !CreateJobRequest {
    if (body.len > 64 * 1024) return error.BodyTooLarge;

    var parsed = try std.json.parseFromSlice(
        CreateJobRequest,
        allocator,
        body,
        .{ .ignore_unknown_fields = false },
    );
    defer parsed.deinit();

    return .{
        .kind = parsed.value.kind,
        .payload = try parsed.value.payload.cloneWithAllocator(allocator),
    };
}
```

O detalhe importante é a política para campos desconhecidos. Se o contrato declara `additionalProperties: false`, o parser deve rejeitar o que não conhece. Se você precisa compatibilidade futura, documente isso no contrato e teste que o handler ignora campos extras de forma intencional.

## Erros também fazem parte do contrato

Muita API documenta apenas o caminho feliz. Em produção, o formato de erro é tão importante quanto o payload de sucesso, porque clientes automatizados dependem dele para retry, mensagem ao usuário e observabilidade.

Padronize pelo menos quatro famílias:

| Código HTTP | Uso | Corpo recomendado |
|---|---|---|
| `400` | JSON malformado | `code`, `message` |
| `401`/`403` | autenticação/autorização | `code`, `message` |
| `404` | recurso não encontrado | `code`, `message` |
| `422` | JSON válido, regra inválida | `code`, `message`, `field` opcional |
| `500` | falha interna | `code`, `message` sem detalhe sensível |

Em Zig, isso combina com `error union`. O handler pode mapear erros de domínio para respostas HTTP sem expor stack trace, SQL, path local ou segredo.

```zig
const ApiError = struct {
    code: []const u8,
    message: []const u8,
    field: ?[]const u8 = null,
};

fn errorResponse(err: anyerror) ApiError {
    return switch (err) {
        error.BodyTooLarge => .{ .code = "body_too_large", .message = "Corpo JSON excede o limite." },
        error.UnknownJobKind => .{ .code = "invalid_kind", .message = "Tipo de tarefa desconhecido.", .field = "kind" },
        else => .{ .code = "internal_error", .message = "Falha interna." },
    };
}
```

Documente esses formatos no OpenAPI. Cliente que só sabe tratar `200` e `500` vai quebrar no primeiro problema real.

## Testando exemplos do contrato

O melhor uso de OpenAPI em um projeto Zig pequeno é transformar exemplos em testes. Se o contrato mostra um payload aceito, o parser deve aceitar. Se mostra um erro esperado, o handler deve devolver aquele código.

Uma abordagem simples:

1. mantenha exemplos JSON em `testdata/openapi/`;
2. referencie os mesmos exemplos no `openapi.yaml`;
3. escreva testes Zig que leem esses arquivos;
4. rode os testes no CI junto com `zig fmt --check` e `zig build test`.

```zig
test "CreateJobRequest aceita exemplo oficial" {
    const allocator = std.testing.allocator;
    const body = @embedFile("../testdata/openapi/create-job-request.json");
    const req = try parseCreateJob(allocator, body);

    try std.testing.expectEqual(JobKind.relatorio, req.kind);
}
```

Esse padrão reduz drift porque o exemplo que aparece para o consumidor é o mesmo que o serviço compila nos testes. Se alguém renomeia `kind` para `type`, o teste quebra antes do deploy.

## Validando o documento no CI

Mesmo sem gerador, valide o `openapi.yaml`. Use uma ferramenta externa no CI e mantenha o Zig livre de dependência pesada. Duas opções comuns são `redocly lint` e `swagger-cli validate`. A escolha importa menos que rodar sempre.

Um pipeline mínimo:

```yaml
name: api-contract
on: [push, pull_request]
jobs:
  contract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npx --yes @redocly/cli lint openapi.yaml
      - run: zig fmt --check src test
      - run: zig build test
```

Se o projeto evita Node no CI, use um container ou binário dedicado. O ponto é que o contrato deve falhar rápido quando alguém remove schema usado por cliente, duplica operação ou escreve YAML inválido.

## Clientes: gerar tudo ou escrever fino?

Gerar um cliente completo pode ser útil para TypeScript, Kotlin ou Python, mas nem sempre vale a pena para Zig. Muitas APIs internas precisam de meia dúzia de funções explícitas, não de centenas de tipos gerados.

Uma camada manual em Zig costuma ser suficiente:

```zig
pub fn createJob(client: *std.http.Client, allocator: std.mem.Allocator, base_url: []const u8, req: CreateJobRequest) !JobAccepted {
    var body = std.ArrayList(u8).init(allocator);
    defer body.deinit();
    try std.json.stringify(req, .{}, body.writer());

    const url = try std.fmt.allocPrint(allocator, "{s}/v1/jobs", .{base_url});
    defer allocator.free(url);

    // Monte request, envie body, cheque status 202 e parseie resposta.
    // Em código real, centralize headers, timeout e mapeamento de ApiError.
    _ = client;
    return error.NotImplemented;
}
```

O contrato ainda ajuda: ele define status esperado, schema de erro, campos obrigatórios e exemplos. A implementação Zig fica pequena e legível.

Para consumidores TypeScript, Java ou Go, pode fazer sentido gerar cliente a partir do OpenAPI. Nesse caso, trate o código gerado como artefato de fronteira, não como modelo de domínio. Ele existe para atravessar HTTP, não para mandar no desenho interno do serviço. Se a equipe mantém serviços em Go também, compare esse limite com o guia de <a href="https://golang.com.br/blog/api-rest-go/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">APIs REST em Go</a> antes de padronizar geração em todos os projetos.

## Versionamento sem quebrar consumidores

OpenAPI torna quebra de contrato visível, mas não decide sua política. Defina regras antes de a API crescer.

Mudanças geralmente seguras:

- adicionar campo opcional em resposta;
- adicionar novo endpoint;
- adicionar novo código de erro documentado;
- aceitar enum novo quando clientes ignoram valores desconhecidos.

Mudanças quebradoras:

- renomear campo obrigatório;
- remover campo de resposta usado por cliente;
- mudar tipo de `string` para `number`;
- trocar `202` por `200` sem aviso;
- transformar campo ausente em `null` ou o inverso;
- mudar semântica de paginação.

Para APIs internas, versionamento pode ser simples: contrato no mesmo monorepo, PR revisado por donos dos consumidores e testes de compatibilidade. Para APIs externas, prefira `/v1`, changelog e janela de migração.

## Segurança e documentação honesta

Contrato não deve vazar segredo. Não coloque token real em exemplo, URL interna sensível, nome de bucket privado ou payload de cliente. Use exemplos plausíveis e sanitizados.

Também não prometa mais estabilidade do que existe. Se uma rota é experimental, marque com `x-beta: true`, tag `experimental` ou descrição clara. Se o endpoint só funciona para uso interno, diga isso. Documentação honesta evita integração errada.

Para endpoints autenticados, documente o mecanismo sem expor implementação desnecessária:

```yaml
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
security:
  - bearerAuth: []
```

Depois alinhe com a prática do serviço: logs sem token, mensagens de erro sem segredo e testes que garantem `401`/`403` previsíveis.

## Checklist de adoção

Antes de dizer que a API tem contrato, confirme:

- existe `openapi.yaml` versionado no repositório;
- rotas públicas têm request, response e erro documentados;
- exemplos JSON são usados por testes Zig;
- CI valida o documento;
- handlers impõem limite de corpo antes de parsear;
- parser e schema concordam sobre campos desconhecidos;
- erros seguem formato comum;
- mudanças quebradoras exigem revisão dos consumidores;
- documentação não contém segredo real;
- pelo menos um cliente ou teste consome o contrato.

OpenAPI em Zig deve deixar o serviço mais previsível, não mais burocrático. Use o contrato para estabilizar fronteiras, impedir drift e ajudar clientes. Mantenha o core em Zig claro, pequeno e explícito.

## Perguntas frequentes

### Preciso gerar código Zig a partir do OpenAPI?

Na maioria dos projetos pequenos, não. O ganho maior vem de validar o contrato, compartilhar exemplos e testar handlers. Gere código quando houver muitos endpoints repetitivos ou quando consumidores externos exigirem SDK.

### OpenAPI substitui testes de integração?

Não. Ele descreve a promessa. Testes provam que o servidor cumpre a promessa. O melhor arranjo é usar exemplos do contrato dentro dos testes.

### Posso usar JSON Schema completo no runtime Zig?

Pode, mas avalie custo. Muitas vezes é melhor validar campos críticos com código Zig explícito e deixar a validação completa do documento para CI e testes de contrato.

### Como lidar com streaming, SSE ou WebSocket?

OpenAPI cobre bem HTTP request/response tradicional. Para [Server-Sent Events](/artigos/zig-server-sent-events-sse/) e [WebSockets](/tutoriais/zig-websockets/), documente handshake, eventos, mensagens e erros com texto e exemplos; não force tudo em schemas quando o formato é contínuo.
