[RES-E001] - Added properties in an object with additionalProperties set to False used in response

Rationale

If the object is defined with additionalProperties set to False then the object will not allow presence of properties not defined on the properties section of the object definition. Adding a definition of a new property makes object sent from the server to the client be considered invalid by a client that is using “old” Swagger specs.

Mitigation

A general suggestion would be to avoid setting additionalProperties to False for request objects as this prevents backward compatible safe object modifications. A possible mitigation to this requires the implementation of a new endpoint that returns the new object schema.

NOTE: Implementing a new endpoint is usually cheap but comes with the complexity of handling multiple versions of similar endpoints.

Example

Old Swagger Specs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
swagger: '2.0'
info:
  title: Minimal Case of RES-E001 Rule
  version: '1.0'
paths:
  /endpoint:
    get:
      responses:
        default:
          description: ''
          schema:
            additionalProperties: false
            properties:
              property_1:
                type: string
            type: object
            x-model: get_endpoint_response

New Swagger Specs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
swagger: '2.0'
info:
  title: Minimal Case of RES-E001 Rule
  version: '1.0'
paths:
  /endpoint:
    get:
      responses:
        default:
          description: ''
          schema:
            additionalProperties: false
            properties:
              property_1:
                type: string
              property_2:
                type: string
            type: object
            x-model: get_endpoint_response

Backward Incompatibility

The following snippet triggers the incompatibility error.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals

from os.path import abspath

from bravado.client import SwaggerClient
from bravado_core.validate import validate_schema_object
from jsonschema import ValidationError
from six.moves.urllib.parse import urljoin
from six.moves.urllib.request import pathname2url

old_client = SwaggerClient.from_url(
    spec_url=urljoin('file:', pathname2url(abspath('old.yaml'))),
)
new_client = SwaggerClient.from_url(
    spec_url=urljoin('file:', pathname2url(abspath('new.yaml'))),
)

object_to_validate = {'property_2': 'v1'}

print('Validating the get endpoint response with the old client: Failed')
try:
    validate_schema_object(
        swagger_spec=old_client.swagger_spec,
        schema_object_spec=old_client.swagger_spec.definitions['get_endpoint_response']._model_spec,
        value=object_to_validate,
    )
    raise RuntimeError('An error was expected')
except ValidationError:
    pass

print('Validating the get endpoint response with the old client: Succeeded')
validate_schema_object(
    swagger_spec=new_client.swagger_spec,
    schema_object_spec=new_client.swagger_spec.definitions['get_endpoint_response']._model_spec,
    value=object_to_validate,
)

NOTE: The code is taking advantage of bravado