Skip to content

Classifications

When Contractual detects a structural difference between a spec and its previous snapshot, it classifies the change into one of three categories:

  • Breaking (major): Existing consumers may fail. Requires a major version bump.
  • Non-breaking (minor): Backward-compatible addition or relaxation. Requires a minor version bump.
  • Patch: Metadata-only change with no behavioral effect. Requires a patch version bump.

Classifications map directly to semver bumps.


Contractual’s built-in structural differ walks two JSON Schema documents recursively and applies these rules to every field comparison.

ClassificationDescriptionExample
field-removedA property present in the old schema is absent in the new schema"email" property deleted from object
required-addedA property has been added to the required array"userId" added to required
type-changedThe type of a field has changed to an incompatible type"string" changed to "integer"
type-narrowedA field that previously accepted multiple types now accepts fewer["string", "null"] changed to "string"
enum-value-removedA value has been removed from an enum array"active" removed from status enum
constraint-tightenedA numeric or string constraint has become more restrictivemaxLength: 255 changed to maxLength: 50; minimum: 0 changed to minimum: 1
additional-properties-deniedadditionalProperties changed from true or absent to falseObject can no longer accept unknown fields
required-field-addedA new required property was added to the schemaNew "taxId" field added and marked required
anyof-option-addedA new option was added to an anyOf arrayNew type added to union
oneof-option-addedA new option was added to a oneOf arrayNew variant added to discriminated union
allof-member-addedA new member was added to an allOf arrayAdditional constraints added
not-schema-changedThe not schema was modifiedExcluded values changed
dependent-required-addedA new dependentRequired constraint was addedNew conditional requirement
ClassificationDescriptionExample
optional-field-addedA new property was added but is not in the required arrayNew "nickname" optional field
enum-value-addedA new value was added to an enum array"pending" added to status enum
constraint-loosenedA numeric or string constraint has become less restrictivemaxLength: 50 changed to maxLength: 255
additional-properties-allowedadditionalProperties changed from false to true or removedObject now accepts unknown fields
new-definitionA new entry was added to $defs or definitions without being referencedNew reusable AddressSchema definition
type-widenedA field now accepts more types than before"string" changed to ["string", "null"]
anyof-option-removedAn option was removed from an anyOf arrayType removed from union (less restrictive)
oneof-option-removedAn option was removed from a oneOf arrayVariant removed from discriminated union
allof-member-removedA member was removed from an allOf arrayConstraints relaxed
dependent-required-removedA dependentRequired constraint was removedConditional requirement relaxed
ClassificationDescriptionExample
description-changedThe description keyword value changedTypo fixed in field description
title-changedThe title keyword value changedField title updated for clarity
examples-changedThe examples or example keyword changedNew example values added
comment-changedThe $comment keyword value changedInternal annotation updated
default-changedThe default keyword value changed when the type is unchangedDefault value updated
format-addedA format keyword was added"email" format added to string field
format-removedA format keyword was removed"uri" format removed from string field
format-changedThe format keyword value changed"date-time" changed to "date"
deprecated-changedThe deprecated annotation changedField marked as deprecated
read-only-changedThe readOnly annotation changedField marked as read-only
write-only-changedThe writeOnly annotation changedField marked as write-only

Contractual’s built-in OpenAPI differ analyzes the structure of two OpenAPI specifications and applies semantic versioning rules to classify changes.

The following changes are classified as breaking (major):

  • Removing an endpoint (DELETE /orders/{id} no longer exists)
  • Changing an HTTP method (POST /orders changed to PUT /orders)
  • Removing a required request body field
  • Adding a required request body field with no default
  • Removing a response field consumers depend on
  • Changing the type of a request or response field
  • Removing a supported content type (application/json removed)
  • Tightening a path, query, or header parameter constraint
  • Changing authentication scheme from optional to required
  • Removing a path parameter
  • Adding a new endpoint
  • Adding an optional request body field
  • Adding a new response field
  • Adding a new content type
  • Loosening a parameter constraint
  • Adding a new query parameter (non-required)
  • Description or summary changes on operations, parameters, or fields
  • Example value changes
  • Deprecation markers added (these are informational; consumers still work)
  • x- extension field changes

When a contract has multiple changes across categories, Contractual applies the highest classification:

Highest classification in changesetVersion bump
Any breaking changemajor (1.2.3 → 2.0.0)
Non-breaking, no breakingminor (1.2.3 → 1.3.0)
Patch onlypatch (1.2.3 → 1.2.4)

If a single PR touches a contract with both breaking and non-breaking changes, the changeset is classified as major. The minor changes are documented in the changelog but do not prevent the major bump.

When multiple changesets exist for the same contract (accumulated across several merged PRs), contractual version aggregates them and applies the highest bump across all changesets before resetting.


Some schema constructs cannot be classified deterministically. Contractual marks these as unknown and flags them for manual review.

Constructs that produce unknown classifications:

  • if / then / else conditional schemas (complex conditional logic)
  • dependentSchemas changes (schema-level conditional dependencies)
  • propertyNames changes (property name validation constraints)
  • unevaluatedProperties / unevaluatedItems changes (Draft 2020-12 keywords)
  • $ref cycles or external references that cannot be resolved at diff time
  • Custom vocabularies and $vocabulary declarations

When a change is marked unknown:

  1. The breaking command exits with code 1 (treated conservatively as potentially breaking)
  2. The auto-generated changeset sets the classification to major as a safe default
  3. The changeset body includes a note explaining which construct triggered the unknown classification

The classification can be overridden by editing the changeset file before merging.


Auto-detection is right most of the time but not always. A field removal might be intentional and already communicated to all consumers. A required field addition might be in a section only internal tooling reads.

To override a classification, edit the changeset frontmatter directly:

---
"orders-api": minor
---
The `legacy_id` field was removed but has been deprecated for 6 months
and no active consumers depend on it per our usage analytics.

Contractual uses whatever classification is in the file. It does not re-run detection at version time. The override is permanent for that changeset.

See Changeset Format for the full frontmatter specification.


  • Overview: How the structural differ works
  • Usage: CLI options and custom differs