Como Comparar Especificações OpenAPI e Bloquear Quebras na CI

Compare duas versões da especificação OpenAPI para identificar alterações que quebram a API antes da fusão. Use o oasdiff ou openapi-diff em CI e, em seguida, elimine a lacuna do contrato com o CLI Apidog.

Ashley Goolam

Ashley Goolam

16 junho 2026

Como Comparar Especificações OpenAPI e Bloquear Quebras na CI

Apidog para empresas

Implantação local

SSO & RBAC

Conforme SOC 2

Explorar Apidog Enterprise

Uma pull request edita openapi.yaml. Os checks de CI ficam verdes. A especificação é válida, sem erros de linter, e dois revisores a aprovam. Três dias depois, um cliente mobile começa a apresentar crashes de null-pointer porque um campo de resposta que costumava estar lá desapareceu. Ninguém o removeu intencionalmente. Alguém renomeou uma propriedade durante um refator, e nada na revisão pegou isso.

Esta é a lacuna que um validador simples nunca vê. Uma especificação pode ser perfeitamente bem-formada e ainda assim quebrar todos os consumidores que dependem dela. A única maneira de saber é comparar a nova especificação com a versão que ela substitui, mudança por mudança, e fazer uma pergunta: isso quebraria um cliente que funcionou ontem? Essa comparação é um diff de OpenAPI, e executá-lo como um portão de merge é um dos checks de maior retorno que você pode adicionar a um repositório de API.

button

O que um diff de OpenAPI realmente compara

Um diff de OpenAPI pega duas especificações, uma base e uma head, e relata o que mudou entre elas. A base é geralmente a especificação em sua branch de destino (o que está ativo). A head é a especificação que sua pull request propõe. Uma boa ferramenta de diff não apenas despeja um delta textual da mesma forma que git diff faria. Ela entende a estrutura do OpenAPI, então pode diferenciar entre edições cosméticas e aquelas que quebram contratos.

Aqui está a distinção que importa. Algumas mudanças são aditivas e seguras:

Os clientes existentes continuam funcionando com todas essas mudanças. Eles enviam o que sempre enviaram e leem o que sempre leram. Outras mudanças são incompatíveis com versões anteriores, e estas são as que causam problemas:

O trabalho de uma ferramenta de diff de OpenAPI é escanear cada caminho, parâmetro, esquema e resposta em ambos os documentos e classificar cada mudança em um desses baldes. Essa classificação é o ponto principal. Um diff de linha bruto enterra um campo required removido sob cinquenta linhas de reformatação. Um diff estrutural o destaca como uma mudança disruptiva e informa em qual caminho ele se encontra.

Se você deseja o modelo mental subjacente para entender por que algumas mudanças quebram e outras não, o guia sobre como versionar e depreciar APIs em escala aborda as regras de compatibilidade em profundidade. A ferramenta de diff é como você aplica essas regras mecanicamente, em vez de esperar que um revisor as lembre.

oasdiff: o cavalo de batalha de código aberto

oasdiff é a ferramenta de código aberto que a maioria das equipes procura. É um único binário Go, é rápido e foi construído especificamente em torno da questão de mudanças disruptivas. Ele lê documentos OpenAPI 3.0 e 3.1 e oferece alguns subcomandos dependendo do que você deseja da comparação.

Os três que você mais usará:

Para um portão de merge, breaking é o que importa. Aponte-o para sua especificação base e sua especificação head:

oasdiff breaking base-openapi.yaml head-openapi.yaml --fail-on ERR

base-openapi.yaml é a especificação da branch de destino e head-openapi.yaml é a da pull request. O subcomando breaking imprime apenas as mudanças incompatíveis. A flag --fail-on ERR é o que transforma isso em um portão: ela faz com que o comando saia com um status diferente de zero quando encontra uma mudança classificada no nível ERR. Saída diferente de zero é o sinal universal que o CI lê como falha.

Esse modelo de severidade vale a pena ser compreendido. oasdiff classifica as mudanças disruptivas em níveis, e ERR é o grave, uma mudança que irá quebrar os clientes. WARN cobre mudanças que podem quebrar alguns clientes, dependendo de como eles são escritos, e INFO é informativo. Você decide onde traçar a linha. --fail-on ERR bloqueia apenas as quebras definitivas. --fail-on WARN é mais rigoroso e também pega as possíveis quebras.

Quando você quer o resumo legível para um changelog ou um comentário de PR, em vez de um passa/falha, o subcomando changelog é a saída mais amigável:

oasdiff changelog base-openapi.yaml head-openapi.yaml

oasdiff tem alguns toques realmente úteis. Ele faz a correspondência de endpoints que sobrevive a parâmetros de caminho renomeados, então não sinaliza {userId} se tornando {id} como uma exclusão-mais-adição quando o caminho é idêntico. Ele pode mesclar esquemas allOf antes de comparar para que a herança não produza ruído. E ele emite mais do que texto simples: HTML, JSON, YAML e Markdown estão todos disponíveis através das flags de saída, o que facilita alimentar o resultado em uma anotação de CI ou um changelog gerado. Para uma ferramenta que você pode colocar em um pipeline em cinco minutos e confiar para ser conservadora sobre o que chama de disruptivo, é difícil de superar.

openapi-diff: a alternativa JVM

Se sua stack já está na JVM, OpenAPITools/openapi-diff é uma sólida segunda opção e vale a pena conhecer. É uma ferramenta baseada em Java (Java 8 e superior) que compara duas especificações OpenAPI 3.x e renderiza a diferença como HTML, Markdown, AsciiDoc, JSON ou texto de console. Você pode executá-la a partir de um JAR compilado, via Maven, Homebrew ou como uma imagem Docker, então ela se adapta a uma variedade de configurações de build sem muito esforço.

Sua comparação vai fundo em parâmetros, respostas, endpoints e métodos HTTP, e traça a mesma linha que interessa a todos: mudanças que mantiveram a compatibilidade com versões anteriores versus mudanças que a quebraram. A CLI é direta:

openapi-diff old-openapi.yaml new-openapi.yaml --fail-on-incompatible

A flag --fail-on-incompatible retorna um código de saída diferente de zero apenas quando uma mudança quebra a compatibilidade retroativa, que é exatamente o comportamento de portão que você deseja. Há um --fail-on-changed mais rigoroso se você preferir falhar em qualquer mudança, e um modo --state que imprime apenas no_changes, compatible ou incompatible quando você quer uma resposta de uma palavra para scripts.

Onde ele brilha é na saída renderizada. Os relatórios em HTML e Markdown são limpos e detalhados, o que torna o openapi-diff uma ótima escolha quando você quer um artefato de diff que um humano realmente lerá, e não apenas um código de saída de CI. A desvantagem é a dependência da JVM e uma inicialização mais pesada do que um binário Go. Se sua equipe já é uma "Java shop", esse custo é zero e a ferramenta se encaixa perfeitamente. Se não, o oasdiff é a opção mais leve. Ambos respondem bem à questão das mudanças disruptivas; escolha o que melhor se adapta ao runtime que você já mantém.

Integrando o diff no CI como um portão de merge

Um diff que você executa manualmente não captura nada, porque o momento em que você se esquece de executá-lo é o momento em que a quebra é enviada. O portão precisa viver no pipeline e ser acionado em cada pull request que toca na especificação.

A única ressalva no CI é que você precisa das duas versões da especificação presentes ao mesmo tempo: a base da branch de destino e a head da PR. O checkout da PR fornece a head. Você puxa a base diretamente do histórico do git sem um segundo checkout:

name: openapi-diff
on:
  pull_request:
    paths:
      - "openapi.yaml"

jobs:
  breaking-changes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Get base spec
        run: git show origin/${{ github.base_ref }}:openapi.yaml > base-openapi.yaml
      - name: Install oasdiff
        run: |
          curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh
      - name: Diff for breaking changes
        run: oasdiff breaking base-openapi.yaml openapi.yaml --fail-on ERR

Alguns detalhes carregam o peso aqui. fetch-depth: 0 puxa o histórico completo para que git show possa alcançar a branch base. A linha git show origin/<base>:openapi.yaml lê a especificação como ela existe na branch de destino e a grava em um arquivo, sem a necessidade de um clone extra. O filtro paths significa que o trabalho só é executado quando a especificação realmente muda, então PRs não relacionadas não pagam por isso. E o passo final é o portão: se oasdiff breaking encontra uma mudança de nível ERR, ele sai com status diferente de zero, o trabalho fica vermelho, e a PR mostra um check falhando antes que alguém clique em merge.

O autor vê precisamente qual mudança quebrou a compatibilidade, em qual caminho, enquanto o código ainda está em revisão. Esse é todo o valor. A quebra é detectada no momento mais barato possível, em vez de em um relatório de crash de cliente.

Nem toda mudança disruptiva é um erro, é claro. Às vezes, você está lançando uma versão principal deliberada e a quebra é intencional. O padrão limpo é bloquear por padrão e exigir uma sobreposição explícita para as exceções: um rótulo na PR, um incremento de versão em info.version ou um fluxo de trabalho aprovado separado. Dessa forma, uma quebra é sempre uma decisão que alguém fez de propósito, nunca um acidente que passou despercebido. O guia de estratégia de versionamento de API explica quando uma quebra justifica uma nova versão principal versus quando ela deve ser simplesmente evitada.

A lacuna que um diff não consegue fechar

Aqui está o limite de todas as ferramentas acima, e é um limite importante. Um diff compara dois arquivos. Ele diz que o novo documento é retrocompatível com o antigo. Não diz nada sobre se o seu serviço em execução realmente corresponde a um ou outro.

Essa é uma falha diferente, e é a que mais incomoda em produção. A especificação promete um campo created_at; a implementação parou silenciosamente de retorná-lo três sprints atrás. A especificação diz que um endpoint retorna 200; o serviço ativo retorna 500 sob uma condição que ninguém testou. O diff está limpo porque ambas as versões da especificação concordam. O contrato e o código não. Um diff estático não tem como saber, porque nunca se comunica com a API.

Fechar essa lacuna significa testar a API ao vivo contra o contrato, não apenas comparar o contrato consigo mesmo. Você gera testes a partir da especificação, os executa contra o serviço em execução e afirma que as respostas reais correspondem às formas documentadas. Isso é teste de contrato, e é a camada que captura a divergência entre o que você escreveu e o que você realmente enviou.

Fechando essa lacuna com Apidog e o Apidog CLI

Apidog é construído para este ciclo, o que o torna um companheiro natural para a etapa de diff, em vez de um substituto. Você importa ou sincroniza sua especificação OpenAPI para um projeto Apidog, e o Apidog pode gerar cenários de teste diretamente da especificação, com asserções derivadas do esquema. Os testes verificam se as respostas reais correspondem aos tipos documentados, campos obrigatórios e códigos de status. Você constrói e mantém esses cenários visualmente, em vez de escrever manualmente um conjunto paralelo de scripts de teste que se desalinham toda vez que o contrato muda.

Como o Apidog mantém design, mocking e testes em um único espaço de trabalho, a especificação permanece a fonte da verdade em todos eles. Você pode baixar o Apidog e importar uma especificação existente para experimentar o ciclo em sua própria API. Se você ainda está decidindo como manter essa especificação sob controle em várias versões em primeiro lugar, o passo a passo sobre controle de versão de uma especificação OpenAPI com Git combina bem com este fluxo de trabalho.

O Apidog CLI é o que executa esses cenários sem interface gráfica em seu pipeline. É um pacote npm:

npm install -g apidog-cli

Você executa um cenário por ID, aponta-o para o ambiente que deseja validar e solicita um relatório compatível com CI:

apidog run \
  --access-token $APIDOG_ACCESS_TOKEN \
  -t <scenarioId> \
  -e <environmentId> \
  -r junit,cli \
  --out-dir ./apidog-reports

O token de acesso autentica a execução e reside em um segredo de CI, nunca em um arquivo commitado. A flag -t seleciona o cenário, -e seleciona o ambiente, e -r junit,cli emite XML JUnit legível por máquina para o seu dashboard de CI, juntamente com a saída de terminal legível para o log de build. Você não adivinha os IDs: você copia o comando exato, com os IDs reais do cenário e do ambiente já preenchidos, da aba CI/CD do cenário no Apidog. Se você quiser a superfície completa de opções, o guia completo do CLI documenta todas as flags, e apidog run --help as imprime sob demanda.

O comportamento do portão segue o mesmo princípio do diff. Quando uma asserção falha, porque uma resposta ao vivo não corresponde mais ao contrato, apidog run sai com um código de erro (diferente de zero). O CI lê o código de saída, marca a etapa como falha e bloqueia o merge. Nenhuma configuração extra. Enquanto a etapa de execução estiver no pipeline, uma regressão de contrato para a linha da mesma forma que um diff de mudança disruptiva.

A sequência completa de pré-merge

Junte as duas metades e você terá um pipeline que captura ambos os tipos de quebra. O diff captura mudanças que quebrariam um cliente ao ler a especificação. O teste de contrato captura um serviço que não honra mais a especificação ao exercitar a API em execução. Execute-os como trabalhos separados:

jobs:
  breaking-changes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - run: git show origin/${{ github.base_ref }}:openapi.yaml > base-openapi.yaml
      - run: curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh
      - run: oasdiff breaking base-openapi.yaml openapi.yaml --fail-on ERR

  contract-conformance:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm install -g apidog-cli
      - name: Run contract tests
        run: |
          apidog run \
            --access-token "$APIDOG_ACCESS_TOKEN" \
            -t 605067 \
            -e 1629989 \
            -r junit,cli \
            --out-dir ./apidog-reports
        env:
          APIDOG_ACCESS_TOKEN: ${{ secrets.APIDOG_ACCESS_TOKEN }}
      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: apidog-report
          path: ./apidog-reports

Os dois trabalhos são executados em paralelo. O trabalho de diff lê arquivos e não precisa de nada além do git, então termina em segundos. O trabalho de conformidade precisa de um ambiente acessível, então geralmente é executado em um build de staging implantado. O if: always() no upload mantém o relatório fluindo mesmo quando os testes falham, que é exatamente quando você deseja lê-lo. Se qualquer um dos trabalhos ficar vermelho, a PR é bloqueada. Para mais informações sobre como executar o CLI em pipelines reais, o guia do Apidog CLI GitHub Actions e o passo a passo mais amplo de pipeline CI/CD aprofundam-se na configuração.

button

Pratique o design de API no Apidog

Descubra uma forma mais fácil de construir e usar APIs