ReferenceCLIapigeniediff
diff
Compares two versions of an OpenAPI, Swagger 2.0 or AsyncAPI schema, of a
Protobuf (.proto) file, or of a GraphQL schema, and reports the changes and
their severity.
Usage
apigenie diff [OPTIONS] SOURCE_FILE DEST_FILE
Options
| Option | Description |
|---|---|
--config |
Path to the configuration file. |
--report |
What to print: status, changelog (default) or detailed. |
--formatter |
html, json, markdown, stylish (default), unix or visualstudio. |
--update-version |
Updates info.version of the destination file to the smallest version compatible with the changes (a new major version for breaking changes, minor for non-breaking and patch for non-functional ones), keeping the rest of the file intact. |
--today |
The date of the comparison (YYYY-MM-DD) for the sunset_passed condition of the OpenAPI rules: a deprecated element whose x-sunset date is on or before it may go. Defaults to the current date; it has no effect on the other formats. |
The command exits with status 1 when there are breaking changes, 0
otherwise, and 2 when a file can’t be read or compared or the configuration is
invalid (the exit statuses shared by the three binaries). With --update-version it prints nothing and exits with 0.
Protobuf and GraphQL files have no version, so --update-version is rejected
for them.
Both files must be OpenAPI 3.x documents, both Swagger 2.0 documents, both AsyncAPI 3.x documents, both Protobuf files or both GraphQL files.
Swagger documents
A file is treated as Swagger when its root object has a swagger field. For
Swagger documents the differ compares the host, the base
path, the schemes, the consumed and produced media types, paths, operations,
parameters, responses and their headers and examples, schemas, security
definitions, security requirements and tags.
Parameters are matched by their location and name, e.g. query:limit, so a
parameter moved from the query to a header is a removed and an added one. An
operation has at most one body parameter, whose name is only documentation:
it is matched as body. Security requirements are alternatives, matched by
the names of their schemes, so their order doesn’t matter. References to
definitions, parameters and responses are followed, and their changes are
reported where they are used as well as where they are defined.
As for OpenAPI documents, the severity of a change to a schema depends on the
direction of the data: parameters, including body parameters, and consumed
media types get the request rules, responses and produced media types the
response ones. Definitions get the stricter of the two, and read-only
properties don’t matter in requests.
The changelog report lists the changed metadata (including the host, the base path, the schemes, the media types and the security of the whole API), the added, removed and modified operations, with the changes of each modified one, and the paths whose own parameters changed.
AsyncAPI documents
A file is treated as AsyncAPI when its root object has an asyncapi field. For
AsyncAPI documents the differ compares servers, channels, operations,
messages with their payloads and headers, security schemes, bindings and
components. Bindings of HTTP, WebSockets, Kafka, AMQP and MQTT are compared
property by property, those of other protocols as whole values; schemas in
formats other than AsyncAPI and JSON Schema, e.g. Avro, are compared as whole
values too.
The severity of a change to a message depends on who sends it. The messages
of receive operations are sent by other parties to the application, those
of send operations by the application. A reply flows the other way than
the messages of its operation. For example, a new required payload property
breaks the senders of a received message, while the receivers of a sent
message can ignore it. Messages used in both directions, or by no operation,
get the stricter severity.
The changelog report lists the changed metadata, and the added, removed and modified servers, channels and operations, with the changes of each modified one. Changes of components are shown where the components are used.
Protobuf files
Files with the .proto extension are compared as Protobuf (proto3) files;
both files must be Protobuf files. The differ compares the package, imports
and options of the files, and their messages, enums, services and extend
blocks, down to fields, oneofs, enum values, methods, reserved numbers and
names, options and comments.
Declarations are matched the way the programs that use them find them:
- messages, enums, services, oneofs and methods by their names;
- fields and enum values by their numbers, which identify them in the encoded data. A renamed field is a modified field; a field that got another number is a removed and an added one;
- extend blocks by the type they extend;
- reserved numbers as sets, so
reserved 1, 2, 3;equalsreserved 1 to 3;.
Type names are resolved before they are compared, so Address,
User.Address and .shop.v1.User.Address are the same type when they name
the same message, and renaming the package is one change rather than a change
of every type. The files are compared on their own: the files they import are
not read, and the types those declare are compared by their names. To compare
imported declarations as well, bundle both
versions first.
With the recommended rules, a change is breaking when it breaks the programs
exchanging encoded messages (the binary or the JSON encoding) or the code
compiled against the generated classes: removing or renaming a declaration,
changing the type, the label or the oneof of a field, the types or the
streaming of a method, the package, or an option that decides the names of the
generated code (e.g. go_package). Adding declarations is non-breaking, and
comments are non-functional. Removing a field or an enum value is
non-breaking when the destination file reserves its number, which is how
Protobuf files retire them.
Changes are reported at their positions in the destination file; what has been removed is reported at the closest enclosing declaration that is still there. The changelog report lists the changed metadata (package, imports, file options), and the added, removed and modified messages, enums, services and extensions, with the changes of each modified one:
shop.proto
19 1 breaking Modified message 'Customer'
19 1 non-breaking Removed field 'fax'
25 3 breaking Modified field 'full_name'
25 3 breaking Modified field name
36 3 non-breaking Modified message 'Address'
39 5 non-breaking Added field 'country'
GraphQL files
Files with the .graphql, .graphqls or .gql extension are compared as
GraphQL schemas; both files must be GraphQL files. The differ compares their
type systems: the root operation types, the types of every kind with their
fields, arguments, input fields, default values, interfaces, union members and
enum values, the directive definitions, the directives applied to all of
these, and descriptions. Operations and fragments are ignored.
Type extensions (extend type, extend schema, …) are merged into what
they extend, so moving a field from a type into an extension, or reordering
definitions, is not a change. Everything is matched by its name. When a
schema has no schema definition, the types named Query, Mutation and
Subscription are its root operation types, so adding a schema definition
that names them changes nothing. The files are compared on their own: types
defined elsewhere are compared by their names.
With the recommended rules, a change is breaking when operations that were
valid become invalid, or when clients can no longer read their results, as
graphql-js’s findBreakingChanges() decides it:
- removing a type, a field, an argument, an input field, an enum value, a union member, an implemented interface, a directive or one of its locations, or changing the kind of a type or a root operation type;
- adding a required argument or input field: one with a non-null type and no default value;
- changing a type in an incompatible way. The type of a field may become
stricter (
StringtoString!,[String]to[String!]!); the type of an argument or input field may become looser (String!toString). Named types must stay the same, and lists must stay lists; - removing the default value of a non-null argument or input field, which operations that omitted it relied on;
- making a directive non-repeatable, or adding
@oneOfto an input type.
Additions are non-breaking, as are changed default values and applied
directives such as @deprecated; descriptions are non-functional.
Changes are reported at their positions in the destination file; what has been removed is reported at the closest enclosing definition that is still there. The changelog report lists the changed metadata (root operation types, the description and the directives of the schema), and the added, removed and modified scalars, objects, interfaces, unions, enums, input objects and directives, with the changes of each modified one:
shop.graphql
36 1 breaking Modified object 'Customer'
44 3 non-breaking Added field 'phone'
36 1 breaking Removed field 'fax'
38 3 non-breaking Modified field 'name'
38 3 non-breaking Modified field type
JSON output
With --formatter json every report is an object whose formatVersion member
is the version of the format. It changes only when a member is removed or
changes meaning (see Compatibility).
The JSON Schema of the three reports is
apigenie-diff-report.json.
--report status:{ "formatVersion": 1, "status": "breaking" }.--report changelog:formatVersion,metadata(which top-level fields changed) and groups of added, modified and removed items named after the elements of the format, e.g.added,modifiedandremovedoperations for OpenAPI orchannels,operationsandserversfor AsyncAPI.--report detailed:formatVersionandchanges, every change in document order:
{
"formatVersion": 1,
"changes": [
{
"type": "key_removed",
"severity": "breaking",
"target": "paths",
"config_key": "paths",
"key": "/pets/{id}",
"line": 9,
"column": 3
}
]
}
API Genie 0.9 and earlier wrote the reports without formatVersion, and the
detailed one as the bare changes array.
Configuration
The differ is configured in the commands.diff block of the
configuration file given with
--config. The block itself configures the differ for OpenAPI documents; its
swagger, asyncapi, protobuf and graphql sections configure the other
formats. Without --config, or without a section for a format, the format is
compared with its recommended rules. A block or section starts only from the
rulesets in its extends, so list recommended there to change only some of
the recommended rules.
diff
The configuration of the differ for OpenAPI documents. It accepts these keys:
| Key | Description |
|---|---|
extends |
Rulesets to start from: recommended. |
rules |
Severities of the changes of the properties of each node type. |
request, response |
Overlays of rules for the data sent by API consumers (parameters, request bodies) and by API providers (responses). Inside callbacks and webhooks the directions are swapped. A component schema takes the direction of the places it is used in (following the references between schemas, in either document); one used both ways, or nowhere, gets the stricter severity. Only node types and properties, no defaults or extensions. |
overrides |
A list of where (location: one pattern or a list of them, and optionally direction: request or response) and rules applied to the matching locations. |
The rules set a severity (breaking, non_breaking, non_functional,
none to ignore the change, or nested to take the highest severity of the
changes inside) for the properties of each node type:
| Node type | Properties |
|---|---|
openapi |
openapi, info, jsonSchemaDialect, servers, paths, webhooks, components, security, tags, externalDocs |
info |
title, summary, description, termsOfService, contact, license, version |
server |
url, description, variables |
serverVariable |
enum, default, description |
components |
schemas, responses, parameters, examples, requestBodies, headers, securitySchemes, links, callbacks, pathItems |
pathItem |
summary, description, parameters, operations, servers |
operation |
tags, summary, operationId, description, externalDocs, deprecated, parameters, requestBody, responses, callbacks, security, servers |
parameter |
name, in, description, required, deprecated, allowEmptyValue, style, explode, allowReserved, schema, content, example, examples |
header |
the properties of parameter, except in and style |
requestBody |
description, content, required |
response |
description, headers, content, links |
mediaType |
schema, encoding, example, examples |
encoding |
contentType, headers, style, explode, allowReserved |
link |
description, server, operationRef, operationId, parameters, requestBody |
example |
summary, description, value, externalValue |
reference |
$ref, summary, description |
schema |
id, dialect, type, properties, required, readOnly, writeOnly, enum, discriminator, xml, and the other JSON Schema keywords |
discriminator |
propertyName, mapping |
securityScheme |
name, description, type, in, scheme, bearerFormat, flows, openIdConnectUrl |
oauthFlows |
implicit, password, clientCredentials, authorizationCode |
oauthFlow |
authorizationUrl, tokenUrl, refreshUrl, scopes |
paths, responses, callback, securityRequirement |
none: the node type itself is set to the rules of its items, e.g. paths: { remove_item: breaking } |
contact, license, tag, externalDocumentation, xml |
their fields |
A property is set to a severity, or to separate severities for add,
remove and modify; lists and maps take add_item, remove_item,
modify_item, add_first_item and remove_last_item. Properties that every
node has, e.g. parameter.name or schema.readOnly, can only be modified, so
they take a severity or modify. A severity can depend on a condition:
{ if: CONDITION, then: SEVERITY, else: SEVERITY }, where the condition is
required (the added or removed item is required), deprecated (it is
deprecated), sunset_passed (it is deprecated and its x-sunset date is on or
before the date of the comparison, --today), or true / false (the new
value of a boolean property).
The extensions property of a node type configures its x- extensions by
name, with * for the extensions that are not listed, and
defaults.extensions applies to all node types.
sunset_passed is how the recommended rules let a deprecated element go once
its window has closed: the remove_item of operations, path items (every
operation deprecated and sunset), parameters, component schemas and
parameters, response properties and their required items, and request enum
values is non-breaking when the condition holds and breaking otherwise. An
x-sunset is an RFC 3339 date (2027-01-06) or date-time next to
deprecated: true; a property may carry them where it is used, as siblings of
its $ref, or where it is defined; an enum value is deprecated when the
schema’s x-deprecated-enum array lists it, and the schema’s x-sunset says
when it may go. Deprecating itself (deprecated: true on an operation, a
parameter, a header or a schema) is non-breaking: it announces the removal,
and the removal is the breaking change. apigenie evolve plan writes documents that follow this
convention.
version: '1.0'
commands:
diff:
extends: [recommended]
rules:
defaults:
extensions: non_functional
pathItem:
operations:
# deprecated operations may be removed
remove_item:
if: deprecated
then: non_breaking
else: breaking
response:
schema:
properties:
# clients reject unknown properties
add_item: breaking
overrides:
- where:
location: /paths/~1internal~1*/**
rules:
operation:
parameters: non_functional
The locations of overrides are JSON pointers into the document, where *
matches any part of a segment and ** any number of segments, e.g.
/paths/~1pets/get/parameters/query:limit,
/paths/**/properties/name or /components/schemas/Pet/**. The Swagger,
AsyncAPI, Protobuf and GraphQL sections accept the same severities, rule
forms and location patterns.
diff.protobuf
The configuration of the differ for Protobuf files. It accepts extends
(recommended), rules and overrides; there are no request and
response overlays. The OpenAPI settings of the diff block do not apply to
Protobuf files; without this block, Protobuf files are compared with the
recommended rules.
The rules set a severity (breaking, non_breaking, non_functional,
none to ignore the change, or nested to take the highest severity of the
changes inside) for the properties of each node type:
| Node type | Properties |
|---|---|
file |
package, imports, messages, enums, services, extends, comment |
import |
kind (public or weak) |
message |
fields, oneofs, messages, enums, extends, reservedNumbers, reservedNames, comment |
field |
name, type, label, oneof, comment |
oneof |
comment |
enum |
values, reservedNumbers, reservedNames, comment |
enumValue |
name, comment |
service |
methods, comment |
method |
inputType, outputType, clientStreaming, serverStreaming, comment |
extend |
fields, comment |
Map fields and the fields of oneofs are configured as field. The comment
of a declaration is its leading and trailing comment, that of a file the
comment of its syntax statement. A property is
set to a severity, or to separate severities for add, remove and modify
(lists: add_item, remove_item, modify_item, add_first_item,
remove_last_item). Properties that every declaration has, e.g. the name,
type and label of a field or the types of a method, take only a severity
or modify. A severity can depend on a condition:
| Condition | Holds when |
|---|---|
reserved |
the number of the removed field or enum value is reserved in the destination file |
deprecated |
the added, removed or modified field or enum value has deprecated = true |
wire_compatible |
the old and the new type (or label) of a field are encoded the same way, e.g. int32 and int64, string and bytes, or a field with and without optional |
true, false |
the new value of a boolean option is true or false (the old value for a removed option) |
Options are configured by their names as written in the file, in the
options property of file, message, field, oneof, enum,
enumValue, service and method; * stands for the options that are not
listed, and defaults.options applies to all node types.
version: '1.0'
commands:
diff:
protobuf:
extends: [recommended]
rules:
defaults:
options: non_functional
field:
# only the encoded data matters: names and compatible types may change
name: non_breaking
type:
modify:
if: wire_compatible
then: non_breaking
else: breaking
options:
json_name: non_breaking
"(validate.rules)": breaking
message:
fields:
# removing a field is breaking even when its number is reserved
remove_item: breaking
overrides:
- where:
location: /messages/Internal*/**
rules:
message:
fields: none
The locations of overrides follow the structure of the file, with
declarations by their names, e.g. /messages/User/fields/email/type,
/messages/User/messages/Address/**, /enums/Status/values/* or
/services/Users/methods/Get.
diff.swagger
The configuration of the differ for Swagger 2.0 documents. It accepts the same
keys as the diff block itself: extends, rules, the request and
response overlays and overrides. The properties are those of the Swagger
node types:
| Node type | Properties |
|---|---|
swagger |
swagger, info, host, basePath, schemes, consumes, produces, paths, definitions, parameters, responses, securityDefinitions, security, tags, externalDocs |
info |
title, description, termsOfService, contact, license, version |
pathItem |
$ref, parameters, operations |
operation |
tags, summary, description, externalDocs, operationId, consumes, produces, parameters, responses, schemes, deprecated, security |
parameter |
name, in, description, required, schema, type, format, allowEmptyValue, items, collectionFormat, and the validation keywords |
response |
description, schema, headers, examples |
header, items |
type, format, items, collectionFormat, the validation keywords, and description for headers |
schema |
title, description, type, format, the validation keywords, items, allOf, properties, required, maxProperties, minProperties, additionalProperties, discriminator, readOnly, xml, externalDocs, example |
securityScheme |
type, description, name, in, flow, authorizationUrl, tokenUrl, scopes |
securityRequirement |
scopes |
contact, license, tag, externalDocumentation, xml |
their fields |
The validation keywords are default, multipleOf, maximum,
exclusiveMaximum, minimum, exclusiveMinimum, maxLength, minLength,
pattern, maxItems, minItems, uniqueItems and enum. For
additionalProperties, add applies when additional properties become
allowed, remove when they become forbidden, and modify to changes of their
schema. The OpenAPI settings of the diff block do not apply to Swagger
documents; without this block, Swagger documents are compared with the
recommended rules.
version: '1.0'
commands:
diff:
extends: [recommended]
swagger:
extends: [recommended]
rules:
swagger:
host: non_breaking
pathItem:
operations:
# deprecated operations may be removed
remove_item:
if: deprecated
then: non_breaking
else: breaking
overrides:
- where:
location: /paths/~1internal~1*/**
rules:
operation:
parameters: non_functional
The locations of overrides follow the structure of the document, with
parameters by their keys and security requirements by the names of their
schemes, e.g. /paths/~1pets/get/parameters/query:limit/maximum,
/paths/~1pets/post/parameters/body/schema/properties/name or
/definitions/Pet/**.
diff.graphql
The configuration of the differ for GraphQL schemas. It accepts extends
(recommended), rules and overrides, like diff.protobuf. The OpenAPI
settings of the diff block do not apply to GraphQL files; without this
block, GraphQL files are compared with the recommended rules. The node
types and their properties are:
| Node type | Properties |
|---|---|
schema |
query, mutation, subscription, types, directiveDefinitions, description |
type |
kind (of types of all kinds) |
scalar |
description |
object |
interfaces, fields, description |
interface |
interfaces, fields, description |
union |
members, description |
enum |
values, description |
enumValue |
description |
inputObject |
fields, description |
field |
type, arguments, description |
argument |
type, defaultValue, description |
inputField |
type, defaultValue, description |
directive |
arguments, repeatable, locations, description |
The arguments of directive definitions are configured as argument. A
severity can depend on a condition:
| Condition | Holds when |
|---|---|
required |
the added or removed argument or input field has a non-null type and no default value; for a default value, the type of the argument or input field is non-null |
compatible |
the old and the new type of a field, argument or input field are compatible (see above) |
deprecated |
the added or removed field, argument, input field or enum value has @deprecated |
true, false |
the new value of repeatable, or of the only argument of an applied directive when it is a boolean (the old value for a removed directive) |
Applied directives are configured by their names, without @, in the
directives property of schema, scalar, object, interface, union,
enum, enumValue, inputObject, field, argument and inputField;
* stands for the directives that are not listed, and defaults.directives
applies to all node types. Directives are compared with their arguments,
whatever the order of the arguments and the form of the strings; the uses of
a repeatable directive are compared together.
version: '1.0'
commands:
diff:
graphql:
extends: [recommended]
rules:
defaults:
directives: non_functional
field:
directives:
# authorization changes matter to clients
auth: breaking
enum:
values:
# deprecated values can be removed
remove_item:
if: deprecated
then: non_breaking
else: breaking
overrides:
- where:
location: /types/Internal*/**
rules:
object:
fields: none
The locations of overrides follow the structure of the schema, with
definitions by their names, e.g. /types/User/fields/email/type,
/types/Query/fields/users/arguments/first/defaultValue,
/types/Status/values/*, /directiveDefinitions/auth/locations/* or
/query.
diff.asyncapi
The configuration of the differ for AsyncAPI documents. It accepts the same
keys as the diff block itself: extends, rules, overrides, and the
receive and send overlays in place of request and response (also as
the direction of an override). The
properties are those of the AsyncAPI node types, e.g. channel.address or
message.payload. The OpenAPI settings of the diff block do not apply to
AsyncAPI documents; without this block, AsyncAPI documents are compared with
the recommended rules.
version: '1.0'
commands:
diff:
extends: [recommended]
asyncapi:
extends: [recommended]
rules:
channel:
address: non_breaking
send:
schema:
properties:
# receivers reject unknown properties
add_item: breaking
overrides:
- where:
location: /channels/internal/**
rules:
message:
payload: non_functional