Detecting API Breaking Changes Automatically in CI with OpenAPI Diff Tools
AI generated
{ }
GET
REST · OpenAPI · CI/CD
Detecting API Breaking Changes in CI
How OpenAPI diff tools catch contract breaks before they reach consumers

A breaking change in a public API rarely happens on purpose, it usually slips in through an unremarkable pull request that was only supposed to fix an internal bug. Diff tools like oasdiff and openapi-diff compare two versions of an OpenAPI specification automatically and reliably catch exactly these contract breaks before they get merged. This article explains the difference to plain Spectral linting, how to set up a concrete CI gate, and how deliberately intended breaking changes get documented and approved through a whitelist mechanism.

16 min read OpenAPI · oasdiff CI/CD Gates

1. Why silent breaking changes are a recurring problem

A breaking change in a public API rarely happens out of malice, it usually slips in through carelessness: a required field gets renamed, an enum value gets removed, a response field silently changes type from string to integer, and all of that happens inside a pull request that was only supposed to fix an internal bug. Without automated checking, the team often only finds out once an external API consumer reports that their client suddenly crashes with a parsing error.

Manual code review alone is not enough here, because an API contract break is frequently hidden in a single, unremarkable diff line pair that a human reviewer can easily miss among dozens of other changes. This is exactly where OpenAPI diff tools come in: they compare the specification of two API versions automatically and reliably, flagging every change that could be incompatible for existing consumers before it is even merged.

2. Linting with Spectral versus contract comparison with diff tools

Spectral and diff tools like oasdiff get confused for each other in practice, even though they solve fundamentally different problems. Spectral is a linter: it checks a single OpenAPI specification against a set of style rules, for example whether every endpoint has a description, whether property names consistently use camelCase, or whether every response code is documented. Spectral has no notion of a previous version of the API, it evaluates only the current state in isolation.

A diff tool like oasdiff or openapi-diff pursues a different goal: it compares two concrete versions of the same specification, typically the state on the target branch against the state in a feature branch, and identifies semantic differences between them. A perfectly lint-compliant schema can still contain a serious breaking change, for instance when a previously optional field suddenly becomes required, because that is stylistically flawless but contractually incompatible. Linting and diffing complement each other, they do not substitute for one another.

3. oasdiff in detail: how it works and what it outputs

Before a diff tool can compare anything, it needs two concrete OpenAPI files: one for the current state of the target branch and one for the state in the pull request. In a Symfony application that generates its API documentation from attributes via NelmioApiDocBundle, this export can be automated through a dedicated console command that runs in the CI pipeline right before the actual diff step.

oasdiff itself is a command-line tool that takes two OpenAPI documents (YAML or JSON) as arguments and outputs a structured list of changes, categorized by severity: info, warning, and breaking. The categorization follows OpenAPI semantics directly, oasdiff knows for example that adding a new optional response field is harmless, while removing an existing field or tightening a validation rule gets classified as breaking.


<?php
declare(strict_types=1);

namespace App\Command;

use Nelmio\ApiDocBundle\ApiDocGenerator;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Yaml;

/**
 * Exports the current OpenAPI specification as a YAML file
 * so it can be compared against the target branch state in the CI pipeline.
 */
#[AsCommand(name: 'api:openapi:export')]
final class ExportOpenApiSpecCommand extends Command
{
    public function __construct(
        private readonly ApiDocGenerator $apiDocGenerator,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $spec = $this->apiDocGenerator->generate();

        file_put_contents(
            'var/openapi/current.yaml',
            Yaml::dump($spec->toArray(), 6),
        );

        $output->writeln('OpenAPI specification exported to var/openapi/current.yaml.');

        return Command::SUCCESS;
    }
}

4. openapi-diff as an alternative: differences from oasdiff

openapi-diff (originally released by OpenAPITools as a Java-based tool) follows the same basic idea as oasdiff, but differs in implementation and ecosystem. While oasdiff, being a Go binary, starts up quickly and integrates easily into Docker images or GitLab CI jobs, openapi-diff runs on the JVM, which in a pure PHP/Symfony environment means an additional runtime dependency that has to be maintained separately.

In terms of content, both tools deliver comparable results when detecting classic breaking changes such as removed endpoints, changed required fields, or tightened enum constraints. The practical difference lies more in output formatting and integration maturity: oasdiff ships with a machine-readable JSON format out of the box that can be parsed directly in a CI script, while openapi-diff is primarily optimized for HTML report output and needs more customization for an automated gate check.

5. A CI gate that blocks PRs on unannounced breaking changes

A CI gate for API contract breaks works on a simple principle: the pipeline job fetches the OpenAPI specification of the target branch (usually main or develop), generates the specification of the current feature branch, runs oasdiff against both, and fails the job with a non-zero exit code as soon as at least one change is classified as breaking. The pull request technically cannot be merged as long as this job is configured as a required check.

What matters for team acceptance is that the error message in the CI log is specific and understandable, not just 'breaking change detected', but naming the exact property, the affected endpoint, and the kind of change. Only then can a developer judge within a few seconds whether the change was genuinely unintentional or deliberate and needs to go through the whitelist mechanism described below.


openapi-breaking-check:
  stage: test
  image: tufin/oasdiff:latest
  script:
    - git show origin/main:openapi/spec.yaml > /tmp/base.yaml
    - oasdiff breaking /tmp/base.yaml openapi/spec.yaml
      --fail-on ERR
      --exclude-ops-with-extension "x-breaking-allowed"
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

6. What counts as a breaking change and what does not

Not every schema change automatically counts as a breaking change, and an overly aggressive diff tool configuration quickly causes alert fatigue in the team. Clearly breaking changes include: removing an endpoint or an HTTP method, adding a new required field to a request body, removing a response field that consumers might rely on, and tightening validation rules such as minimum or maximum lengths.

Not breaking, on the other hand, are adding a new optional field, adding a new endpoint, loosening a validation rule (say, raising a maximum length instead of lowering it), or adding a new additional enum value in a response, provided the client is implemented to tolerate unknown enum values. This distinction follows the principle that a change is breaking when it breaks an existing, correctly implemented client, regardless of whether the change itself was reasonable or intentional.

7. A whitelist mechanism for deliberately accepted breaking changes

In practice there are legitimate cases where a breaking change is deliberately wanted, for example when finally removing a field that has been marked deprecated for a long time as part of an announced major version. A rigid CI gate that blocks every breaking change without exception would prevent such planned changes too, and would tempt the team to just disable the check when in doubt, which defeats its actual purpose.

The workable approach is a whitelist mechanism: a developer who deliberately wants to introduce a breaking change explicitly registers the affected change in an exception list (for instance through a schema extension like x-breaking-allowed or a separate configuration file) and has to provide a short justification in the pull request explaining why the change is necessary and announced. The CI gate recognizes the exception and lets the pipeline turn green, but the change stays visible and documented in the review history instead of slipping through silently.

8. Fitting it into the team workflow: PR justification and review

To keep the whitelist mechanism from turning into a convenient way to bypass the gate, every exception should be tied to a mandatory justification in the pull request workflow and, ideally, to a second reviewer's sign-off. A simple pattern is a pull request template with a dedicated 'Breaking Changes' section that must be filled in whenever the CI gate reports an exception, and that gets explicitly countersigned by the API owner or a designated reviewer team.

This process does not make breaking changes impossible, it makes them visible and deliberate instead of letting them slip through by accident. For external API consumers it is also worth documenting accepted breaking changes in a public changelog and announcing them with a reasonable transition period before the new major version actually goes live.

9. The limits of automated detection at a glance

Automated diff checking does not replace a full understanding of the contract, it reliably detects structural changes in the schema but not semantic behavior changes that never show up in the OpenAPI specification, such as a changed sort order in a response list or altered error handling on edge-case timeouts. The table below places the discussed tools and mechanisms side by side.

Aspect Spectral oasdiff/openapi-diff CI gate with whitelist
Checks Style of a single spec Semantic difference between two specs Combination of diff result and exception list
Detects breaking changes No Yes, categorized by severity Yes, with a deliberate exception path
Typical usage Before every commit/PR Against target branch in the CI pipeline As a required check before merge
Reaction to a finding Warning/error in the lint report Non-zero exit code on a breaking change Pipeline fails unless documented as an exception

Mironsoft

OpenAPI design, Symfony APIs, and API security

APIs that external teams can integrate without back-and-forth questions?

We review existing REST APIs for inconsistent error formats, missing OpenAPI documentation, and security gaps, then build an API that is clearly documented, versioned, and hardened against abuse.

API Review

Checking the OpenAPI spec, error formats, and status codes for consistency.

Symfony Implementation

Using DTOs, Serializer, and Validator for clean, type-safe request/response models.

Security Audit

Hardening rate limiting, auth schemes, and input validation against real attack surfaces.

10. Summary

OpenAPI Diff Tools: The Essentials at a Glance

Core difference

Spectral checks the style of a single specification, diff tools like oasdiff compare two versions and catch real contract breaks.

Practical tool

oasdiff, a fast Go binary, drops directly into CI pipelines and produces machine-readable JSON output.

CI gate principle

The pipeline job blocks the merge as soon as an unapproved change is classified as breaking.

Whitelist rule

Deliberate breaking changes are registered explicitly and justified in the pull request instead of bypassing the gate.

11. FAQ: OpenAPI Diff Tools: The Essentials at a Glance

1What is the difference between Spectral and oasdiff?
Spectral checks a single OpenAPI specification against style rules, oasdiff compares two versions of the same specification and detects semantic, contract-breaking differences. Both tools complement each other but do not replace one another.
2Does oasdiff detect the removal of a required field?
Yes, removing a response field or adding a new required field to a request are classified as breaking by oasdiff by default.
3Do I have to maintain my OpenAPI spec manually to diff it?
No, in a Symfony application using NelmioApiDocBundle the specification can be generated automatically from code attributes and exported in the CI pipeline before the diff step runs.
4What happens if a breaking change is deliberately wanted?
A whitelist mechanism lets the change be registered explicitly as an exception, along with a justification in the pull request, so the CI gate still lets the pipeline pass.
5Is openapi-diff a good alternative to oasdiff?
Functionally yes, but openapi-diff runs on the JVM, which in a pure PHP environment means an extra runtime dependency. oasdiff, being a Go binary, is usually easier to integrate into existing CI pipelines.
6Does the CI gate also block harmless changes like new optional fields?
No, adding a new optional field or a new endpoint is not classified as breaking and passes the gate without issue.
7How do I stop the whitelist from becoming a convenient bypass?
By requiring a mandatory written justification in the pull request for every exception, and ideally a sign-off from a second reviewer or the API owner.
8Can I use the CI gate for internal APIs consumed by only one team?
Yes, it becomes especially useful once multiple teams or external partners depend on the API. For a purely internal API controlled by a single team, the benefit is smaller.
9What is the difference between a breaking and a non-breaking change with enums?
Removing an existing enum value counts as breaking, adding a new enum value in a response usually counts as non-breaking, provided the client is implemented to tolerate unknown values.
10Does an OpenAPI diff also catch behavior changes that never show up in the schema?
No, diff tools only check the structural specification. Semantic behavior changes such as a changed sort order are not detected, that requires additional contract or integration tests.