OpenAPI 3.0 and 3.1 Support
Contractual’s built-in OpenAPI differ supports both OpenAPI 3.0 and 3.1 without any extra configuration. Point type: openapi at a spec file and all CLI commands — diff, breaking, changeset, version — work regardless of which version the spec declares.
How resolution works
Section titled “How resolution works”Before comparing two specs, Contractual uses @redocly/openapi-core to load and dereference each document. This step:
- Resolves all
$refpointers, including cross-file references - Normalizes multi-file specs into a single in-memory document
- Handles both 3.0 and 3.1 parsing rules correctly
- Produces a plain JavaScript object that the structural differ can walk
You do not need to install @redocly/openapi-core yourself. It ships as a dependency of @contractual/differs.openapi.
Package architecture
Section titled “Package architecture”The differ system is split into three packages that compose together:
@contractual/differs.core Walker, classifiers, ref-resolver, result assembly. Shared by all differs.
@contractual/differs.json-schema Thin wrapper over core. Same public API as before. Handles standalone JSON Schema files.
@contractual/differs.openapi Uses Redocly to resolve specs (3.0 and 3.1). Structural layer: paths, operations, parameters, responses. Delegates schema-level diffing to the core walker.You interact with the differ through the Contractual CLI or GitHub Action. You only import the packages directly if you are integrating programmatically.
Programmatic API
Section titled “Programmatic API”import { diffOpenApi } from '@contractual/differs.openapi';
const result = await diffOpenApi('old-spec.yaml', 'new-spec.yaml');
console.log(result.suggestedBump); // 'major' | 'minor' | 'patch' | 'none'console.log(result.summary);// { breaking: 1, nonBreaking: 0, patch: 0, unknown: 0 }
for (const change of result.changes) { console.log(`[${change.severity}] ${change.message}`);}If you have already resolved the spec objects yourself (for example, from a custom bundler), use diffOpenApiObjects instead to skip the Redocly resolution step:
import { diffOpenApiObjects } from '@contractual/differs.openapi';
const result = diffOpenApiObjects(oldSpecObject, newSpecObject);What the structural layer detects
Section titled “What the structural layer detects”The structural layer compares the HTTP API surface — paths, operations, parameters, request bodies, and responses. Schema-level changes within those locations are delegated to the core walker.
Breaking changes (major)
Section titled “Breaking changes (major)”| Change | Example |
|---|---|
| Path removed | DELETE /orders/{id} path item removed |
| Operation removed | GET /orders/{id} operation removed from existing path |
| Parameter removed | status query parameter removed |
| Parameter required changed | limit changed from optional to required |
| Request body added | Endpoint that previously took no body now requires one |
| Response removed | 200 response removed from an operation |
Non-breaking changes (minor)
Section titled “Non-breaking changes (minor)”| Change | Example |
|---|---|
| Path added | New /invoices path added |
| Operation added | DELETE /orders/{id} added to existing path |
| Parameter added | New optional page query parameter added |
| Request body removed | Endpoint no longer requires a body |
| Response added | New 202 Accepted response added to an operation |
| Server changed | servers[].url updated |
Schema-level changes
Section titled “Schema-level changes”Changes inside request body schemas, response schemas, and parameter schemas are classified by the core walker using full JSON Schema semantics:
- Type narrowing and widening
- Enum value additions and removals
- Constraint tightening and loosening
- Required field additions and removals
- Property additions and removals
- Composition keyword changes (
anyOf,oneOf,allOf,not)
See Classifications for the complete rule set.
OpenAPI 3.1-specific constructs
Section titled “OpenAPI 3.1-specific constructs”OpenAPI 3.1 aligns with JSON Schema 2020-12. The following 3.1-specific features are handled correctly by the core walker.
Type arrays
Section titled “Type arrays”OpenAPI 3.0 expresses nullable types with the nullable: true keyword. OpenAPI 3.1 uses a type array instead:
amount: type: number nullable: trueamount: type: ["number", "null"]The walker normalizes both forms before comparison. A change from type: number, nullable: true (3.0) to type: ["number", "null"] (3.1) with no other difference produces no diff. Removing "null" from the type array is classified as type-narrowed (breaking).
| Change | Classification |
|---|---|
"null" added to type array | Non-breaking (type widened) |
"null" removed from type array | Breaking (type narrowed) |
| Type changed across the array | Breaking |
Numeric exclusiveMinimum and exclusiveMaximum
Section titled “Numeric exclusiveMinimum and exclusiveMaximum”OpenAPI 3.0 (following JSON Schema Draft-07) uses exclusiveMinimum: true as a boolean alongside minimum. OpenAPI 3.1 uses a numeric value directly:
price: type: number minimum: 0 exclusiveMinimum: true # means: value must be > 0price: type: number exclusiveMinimum: 0 # means: value must be > 0The walker understands both forms. Tightening the exclusive bound is classified as constraint-tightened (breaking). Loosening it is constraint-loosened (non-breaking).
jsonSchemaDialect
Section titled “jsonSchemaDialect”OpenAPI 3.1 documents can declare a jsonSchemaDialect field at the top level. This is an informational annotation — changes to it are classified as patch.
Full JSON Schema 2020-12 keywords in schemas
Section titled “Full JSON Schema 2020-12 keywords in schemas”OpenAPI 3.1 schemas may use any JSON Schema 2020-12 keyword. The core walker supports:
| Keyword | Notes |
|---|---|
prefixItems | Tuple validation; replaces array-form items from Draft-07 |
unevaluatedProperties | Changes flagged as unknown (require review) |
unevaluatedItems | Changes flagged as unknown (require review) |
dependentRequired | Breaking when added, non-breaking when removed |
dependentSchemas | Changes flagged as unknown (require review) |
minContains / maxContains | Constraint tightening/loosening rules apply |
contentEncoding, contentMediaType, contentSchema | Patch |
deprecated, readOnly, writeOnly | Patch |
Using 3.1 specs in practice
Section titled “Using 3.1 specs in practice”No configuration change is needed. Contractual detects the spec version from the document and routes it through the same pipeline:
contracts: - name: orders-api type: openapi # works for 3.0 and 3.1 path: ./specs/orders.openapi.yamlA 3.1 spec starts with:
openapi: "3.1.0"info: title: Orders API version: "1.0.0"A 3.0 spec starts with:
openapi: "3.0.3"info: title: Orders API version: "1.0.0"Both are handled identically from the Contractual configuration perspective. All CLI commands and the GitHub Action work without modification.
Migrating a spec from 3.0 to 3.1
Section titled “Migrating a spec from 3.0 to 3.1”When you migrate a spec from 3.0 to 3.1, you may change nullable: true to type arrays and update exclusiveMinimum/exclusiveMaximum from booleans to numbers. The differ understands both forms, so a semantically equivalent migration produces no breaking changes.
Take a snapshot before the migration so that the differ has a 3.0 baseline to compare against:
# While still on 3.0contractual diff # should show no changes
# After migrating to 3.1contractual diff # should still show no changes if semantics are equivalentIf the migration accidentally tightens or removes a constraint, contractual diff will surface it.
Next steps
Section titled “Next steps”- Breaking Change Detection: How the structural differ works end to end
- Classifications: Full rule set for every change type
- OpenAPI + TypeScript recipe: Generate a TypeScript client from a 3.1 spec