Adding non-required property is considered a breaking change

Issue #15 closed
Leandro Aparecido created an issue

Given the following API specs:

openapi-old.yaml:

openapi: 3.0.1
info:
  title: Old
  version: v1
  description: Old spec
paths:
  /test:
    summary: Test
    description: Description
    post:
      operationId: testOperation
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  description: Token supplied by user to authorize the request
                  type: string
              required:
                - token
      responses:
        '201':
          description: If the request has been registered successfully

openapi-new.yaml:

openapi: 3.0.1
info:
  title: New
  version: v1
  description: New spec
paths:
  /test:
    summary: Test
    description: Description
    post:
      operationId: testOperation
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  description: Token supplied by user to authorize the request
                  type: string
                newProperty:
                  type: string
              required:
                - token
      responses:
        '201':
          description: If the request has been registered successfully

Running openapi-diff openapi-old.yaml openapi-new.yaml

Yields the following output:

Breaking changes found between the two specifications:
{
    "breakingDifferences": [
        {
            "type": "breaking",
            "action": "remove",
            "code": "request.body.scope.remove",
            "destinationSpecEntityDetails": [
                {
                    "location": "paths./test.post.requestBody.content.application/json.schema",
                    "value": {
                        "type": "object",
                        "properties": {
                            "token": {
                                "description": "Token supplied by user to authorize the request",
                                "type": "string"
                            },
                            "newProperty": {
                                "type": "string"
                            }
                        },
                        "required": [
                            "token"
                        ]
                    }
                }
            ],
            "entity": "request.body.scope",
            "source": "json-schema-diff",
            "sourceSpecEntityDetails": [
                {
                    "location": "paths./test.post.requestBody.content.application/json.schema",
                    "value": {
                        "type": "object",
                        "properties": {
                            "token": {
                                "description": "Token supplied by user to authorize the request",
                                "type": "string"
                            }
                        },
                        "required": [
                            "token"
                        ]
                    }
                }
            ],
            "details": {
                "differenceSchema": {
                    "type": "object",
                    "properties": {
                        "newProperty": {
                            "type": [
                                "array",
                                "boolean",
                                "integer",
                                "null",
                                "number",
                                "object"
                            ]
                        },
                        "token": {
                            "type": "string"
                        }
                    },
                    "required": [
                        "newProperty",
                        "token"
                    ]
                }
            }
        }
    ],
    "breakingDifferencesFound": true,
    "nonBreakingDifferences": [],
    "unclassifiedDifferences": []
}

Adding a non-required property shouldn’t be considered a breaking change.

Comments (3)

  1. Ben Sayers

    I’ll explain the breaking change by way of example. In the original schema this request was accepted:

    POST /test
    {
      "token": "a-token",
      "newProperty": 123
    }
    

    This is because by default JSON Schema allows all values in all properties, and by adding keywords you are placing constraints on the allowed values. So since the original schema did not place any constraints on the property newProperty, it was allowed to be any value.

    However, the new schema does not accept this request because it applies a constraint to this property, only allowing it to be of type string. This means that this property can no longer be of type number. It is also not allowed to be array, boolean, integer, null or object. You can see this in the differenceSchema returned by the tool. Any client sending a request with newProperty with any of these now unsupported types will have their requests considered invalid if you make this change, and so therefore we consider this a breaking change.

    If you are confident no client is sending a property with one of these now unsupported types you can safely make the change - the tool has done it’s job of helping you make informed decisions about what impact a change to your spec will have.

    You could also consider using the additionalProperties keyword in order to prevent undefined properties from being accepted. This would allow you to add properties into the properties list of the schema and not have them be flagged as breaking changes. For example, consider this old request body schema:

    type: object
    properties:
      token:
        type: string
    required:
     - token
    additionalProperties: false
    

    Compared to this new request body schema:

    type: object
    properties:
      token:
        type: string
      newProperty
        type: string
    required:
     - token
    additionalProperties: false
    

    This would not be flagged as a breaking change because the original schema did not allow the property newProperty to be any value.

    Just be aware that if you use this approach it will mean that when you do want to add new properties to your API you will be forced to make a change to the spec first before the property will be allowed, you won’t be able to start with the client. You will have to consider if this is a tradeoff you are willing to make.

    Hope that clears things up, let me know if you have any further questions.

  2. Log in to comment