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; equals reserved 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 (String to String!, [String] to [String!]!); the type of an argument or input field may become looser (String! to String). 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 @oneOf to 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, modified and removed operations for OpenAPI or channels, operations and servers for AsyncAPI.
  • --report detailed: formatVersion and changes, 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