Keyword Support
OpenAPI contracts may contain the logical keywords anyOf
, allOf
, and oneOf
in schema definitions, which are used to support rich domain models, validate a value against multiple criteria and generally encourage reuse. This section explains in detail how we approach handling these keywords in PactFlow and what you can do.
Supported OpenAPI Keywordsβ
PactFlow supports all three allowed OpenAPI Schema keywords.
From the JSON Schema website, the validation these keywords provide can be summarized as follows:
allOf
: (AND) Must be valid against all of the subschemasanyOf
: (OR) Must be valid against any of the subschemasoneOf
: (XOR) Must be valid against exactly one of the subschemas
Examplesβ
This project contains working examples of keyword support, as well as various other OpenAPI use cases.
General Adviceβ
When using oneOf
, you must consider the discriminator
.
One of the challenges with the use of oneOf
when testing a given JSON data structure against the OpenAPI, is that it should only match a single schema. However, a consumer may (and in many cases, is expected to) specify only a subset of the data from a provider in their tests - the data they need for their use cases. This increases the chances it will match multiple schemas and fail.
The discriminator
keywordβ
We can address ambiguity in oneOf
schemas by using the discriminator
keyword. The discriminator
keyword clarifies the potential matching types, by using the value of a single property to discover the correct schema to match.
In the example below, we support a polymorphic response for a resource via two subschemas - a Dog
or a Cat
.
responses:
"200":
description: successful operation
content:
"application/json":
schema:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: petType # <- property used to discriminate between response types
required:
- petType # <- it must be required
In the definition of the subschemas, you can then specify the discriminator value either as an enum
or a const
, as in the example below:
components:
schemas:
Dog:
type: object
properties:
petType:
const: Dog # <- discriminator value
name:
type: string
owner:
type: string
bark:
type: string
Cat:
type: object
properties:
petType:
const: Cat # <- discriminator value
name:
type: string
meow:
type: string
This strategy can be used with allOf
in the case of inheritence also. This would allow the following JSON payload to match one of the schemas (the Cat
schema):
{
"petType": "Cat",
"name": "furry"
}
Without the use of discriminator
, this would match both schemas and fail the validation.
How to use discriminator
β
These are the following requirements and limitations of using the discriminator
keyword:
mapping
in discriminator object is not supported.- "implicit" discriminator values are not supported.
oneOf
keyword must be present in the same schema.discriminator
property should berequired
either on the top level, or in alloneOf
subschemas.- each
oneOf
subschema must have theproperties
keyword withdiscriminator
property. The subschemas should be either inlined or * included as direct references (only$ref
keyword without any extra keywords is allowed). - schema for
discriminator
property in eachoneOf
subschema must beconst
orenum
, with unique values across all subschemas.
Not meeting any of these requirements would fail schema compilation.
Keyword supportβ
The following section delves into additional detail on how we support the keywords, the inherent complexity and the tradeoffs we have taken.
allOf
β
The primary use case for allOf
is the ability to reuse types via composition, inheritance and polymorphism.
The following example is taken from the OpenAPI specification, to demonstrate the common use cases for composition, inheritance and polymorphism. It specifies Cat
and Dog
types, which extend a general Pet
base type. This schema could be used in a response payload, communicating the possible types an endpoint may return.
Please take note: this schema won't pass the stringent rules defined for discriminator
above, as it relies on an implicit discriminator, which PactFlow does not support.
components:
schemas:
Pet:
type: object
discriminator:
propertyName: petType
properties:
name:
type: string
petType:
type: string
required:
- name
- petType
Cat: ## "Cat" will be used as the discriminator value
description: A representation of a cat
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
huntingSkill:
type: string
description: The measured skill for hunting
enum:
- clueless
- lazy
- adventurous
- aggressive
required:
- huntingSkill
Dog: ## "Dog" will be used as the discriminator value
description: A representation of a dog
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
packSize:
type: integer
format: int32
description: the size of the pack the dog is from
default: 0
minimum: 0
required:
- packSize
To validate JSON against an allOf
definition, the data must be valid against all subschemas.
The following JSON body passes this validation:
{
"name": "Rusty",
"petType": "Dog",
"packSize": 7
}
These are able to work because the defined schemas are "open" by default. What does "open" mean?
Open Schemas and additionalProperties
β
From https://json-schema.org/understanding-json-schema/reference/object.html#additional-properties:
The
additionalProperties
keyword is used to control the handling of extra stuff, that is, properties whose names are not listed in theproperties
keyword or match any of the regular expressions in thepatternProperties
keyword. By default any additional properties are allowed.
This last statement is what we should pay attention to - by default, additional properties are allowed. This is what allows the use case above to work.
packSize
is not a property defined in the Pet
schema, and name
and petType
are not defined in the Dog
schema. However, as additionalProperties
are allowed by default, the JSON payload matches both branches of the allOf
schema independently.
Let's explore this a bit more with a simple example to better illustrate the point.
Exampleβ
Given this schema:
allOf:
- title: time
type: object
properties:
time:
type: string
- title: date
type: object
properties:
date:
type: string
With an open schema (as above), the following JSON will pass validation
{
"time": "08:15:00+06:00",
"date": "2022-01-22"
}
And so will
{
"date": "2022-01-22"
}
And
{
"temperature": 25, // <- wait, where did temperature come from?
"unit": "C" // ...and this!
}
And, funnily enough,
{}
This does not pass, because date is not a string:
{
"temperature": 25,
"unit": "C",
"date": 22 // <- not a string!
}
Adding the required
keyword to the 2 properties on the schema improves things a bit
allOf:
- title: time
type: object
properties:
time:
type: string
required:
- time
- title: date
type: object
properties:
date:
type: string
required:
- date
the new minimum JSON is narrowed to
{
"time": "08:15:00+06:00",
"date": "2022-01-22"
}
But you can still add other arbitrary properties - and this is problematic for testing tools like PactFlow.
PactFlow does not allow "open" schemasβ
In most cases on the Internet you wonβt see βclosedβ schemas because OpenAPI's primary use case is documentation and SDK generation where this doesn't really matter. Closing the schema also prevents these important scenarios.
If you consider what PactFlow's job is, it is to prevent a consumer expecting something that provider cannot support! If the consumer needs a property not present in the schema, we need to be able to detect this situation and prevent it.
Therefore, PactFlow must set additionalProperties
to false
on response bodies, otherwise we would provide false positives and a useless feature.
PactFlow automatically closes all schemas
As noted, this breaks the original example and use case above. By disallowing additional properties on each schema, we end up with this unfortunate situation:
{
"name": "Rusty", // β
Matches the Pet schema, β but not Dog
"petType": "Dog", // β
Matches the Pet schema, β but not Dog
"packSize": 7 // β Does not match the Pet schema, β
matches Dog
}
To work around this issue, we use a relatively new JSON schema feature called unevaluatedProperties
on all allOf
schemas. This has the effect of extending the closed schemas, allowing us to treat the allOf
as if it were a single schema and π.
oneOf
and anyOf
β
These keywords enjoy support out of the box, with the minor consideration for the use of discriminators
described above.
Transformations PactFlow applies to OpenAPI documentsβ
Following the discussion in allOf
, in order for PactFlow to perform its compatibility checks, support these keywords and other OpenAPI features, it needs to perform a number of transformations on the document prior to validation.
The transformations it applies to are as follows:
- Sets
additionalProperties
in your OpenAPI tofalse
on any response body, to ensure a consumer won't get false positives if they add a new field that isn't actually part of the spec. - Removes
required
properties from provider responses, as otherwise all consumers would be required to consume the entire provider response! - Sets
unevaluatedProperties: true
onallOf
schemas. This has the effect of extending the (guaranteed to be closed) schemas, allowing us to match against a single composite schema. - Ensures any polymorphic types have an appropriately configured
discriminator
setup (as described above).
The consequences of the above transformations are:
- The transformed OpenAPI will be slighly different to what you provided to PactFlow.
- Additional validations will be performed that may pass other tools (such as the Swagger Editor).
- The
allOf
semantics are slightly modified as described above.