OpenAPI Discrimintors
- Bookmark
- Subscribe
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
OpenAPI Discrimintors
I have been trying to figure out how to handle a situation where a query string parameter changes the response object. Here's my example:
The default response is this:
endpoint: http://myapi.com/pets
response:
{
pet:{
name: "barry",
color: "white",
age: 17
}
}
It can also have a query string parameter where it adjusts the response as follows:
endpoint: http://myapi.com/pets?attributes=name,color
response:
{
pet:{
name: "barry"
}
}
It also supports comma separated lists for the parameter as well like this:
endpoint: http://myapi.com/pets
response:
{
pet:{
name: "barry",
color: "white"
}
}
Is there a way to describe this scenario using discriminators? I know that I can list the responses as optional, but that wasn't ideal.
Thanks!
- Bookmark
- Subscribe
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
Hi @silverbullet2,
I’m not 100% sure what you are trying to achieve in your example.
Are you trying to allow the consumer specify the properties that they are interested in and only return those properties?
`http://myapi.com/pets` --> return all pets according to the defined response schema
`http://myapi.com/pets?attributes=name,color` --> return all pets and only return the `name` and `color` property for each pet
Or are you trying to return two or more different types of objects (aka polymorphism)? If so then you would use `oneOf` or `anyOf` to describe those schemas. If so, then you may want to use a discriminator. You can find some examples of that at https://swagger.io/specification/#discriminator-object
Or are you looking for inheritance by defining a base schema, and then inherit from it using `allOf`? You can also use a discriminator if needed.
An rough example of the inheritance with a discriminator could be to have a base "Insurance" schema and then to extend the insurance schema using `allOf`:
type: object
description: Insurance
discriminator:
propertyName: insuranceType
mapping:
house: '#/components/schemas/HouseInsurance'
car: '#/components/schemas/CarInsurance'
travel: '#/components/schemas/TravelInsurance'
properties:
policyNumber:
description: The insurance policy number
type: string
example: IXP12345678
startAt:
type: string
format: date-time
pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[012][0-9]:[0-5][0-9]:[0-5][0-9]Z$'
description: The date-time from which this policy becomes active
endAt:
type: string
format: date-time
pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[012][0-9]:[0-5][0-9]:[0-5][0-9]Z$'
description: The date-time from which this policy ceases
allOf:
- $ref: ./Insurance.yaml
- type: object
description: Car Insurance
properties:
make:
description: The make of care
type: string
example: Nissan
range:
description: car model range
type: string
example: hatchback
model:
description: car model
type: string
example: micra
registrationNumber:
description: the registration number of the vehicle
type: string
example: 00G1011
value:
type: number
format: double
description: The value of the vehicle
example: 2000.0
In general, discriminators can add complexity to the design and they must be used in conjunction with `anyOf`, `oneOf`, `allOf`. The discriminator must apply to the same level of the schema it is declared.
- Bookmark
- Subscribe
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
@frankkilcommins, I am trying to implement the first option you gave. I want to allow the consumer to specify the properties they are interested in and only return those properties. Is there a way to utilize discriminators for that or what would be the best way to accomplish this?
- Bookmark
- Subscribe
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
Discriminators would generally not be used where you want to allow the consumer describe the shape of the response representation. Think of them more as a shortcut to possibly allow a consumer make a choice from a selection of possible pre-defined schemas. This does not really fit into the example you describe as you just want to reduce the representation of a single schema.
There is no definitive right way to achieve your goal, but a common technique is to have a query parameter (named something like ‘fields’ or ‘properties’ or ‘attributes’ or ‘select’) to allow a consumer specify a subset of properties they would like to be returned with a successful response, rather than the full representation of the resource.
It’s up to you as the API provider to decide what query parameter naming to choose, and to decide if you limit to root level properties or also enable the consumer to select subset of properties within nested objects or arrays that may be returned in the representation. If you’re going to allow selection to subsets within nested objects or arrays, then it would be common to apply a sub-selector expression (generally this would be based on XPath syntax or object.member dot notation)
Some examples:
http://someapiurl.com?fields=color,name –> return all pets and only the color and name properties
http://someapiurl.com?fields=color,name,location/city OR http://someapiurl.com?fields=color,name,location.city –> return all pets and only the color and name root level properties as well as the city property from the location object.
http://someapiurl.com?fields=color,name,hobbies(id, name) –> return all pets and only return the color, name properties, and just the id and name properties for items in the hobbies array
It’s also relatively common that providers leverage bits and pieces from specifications like OData, SCIM or syntaxes like Lucene for advanced filtering/querying (these approaches can increase complexity however).
Things to keep in mind:
- URLs have limits so work to ensure that consumers can not hit URL limits (good practice is to ensure that any combination of params will not result in length greater than ~2000 characters)
- You will need to validate request to ensure that the selector parameter is valid and return the appropriate 400 series HTTP Status code if it contains an error or is otherwise invalid
If you wanted to go full query orientated, then you could consider GraphQL which has it’s own pro’s an con’s.
In general, I would always promote good design where the minimal representation to address the business goal is returned. Separate your APIs into separate resources (or sub-resources), giving your consumer the ability to make more concise calls. Progressively enhance the consumer experience with options to expand or embed additional resources when appropriate.
Ensure you leverage HTTP caching when/where possible to ensure that consumers can cache representations while its safe to do so.
I hope this helps you achieve what you need to do with your API.
- Bookmark
- Subscribe
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
We are already doing as you describe and only allow selection within the root level, not any of the nested arrays. The issue I am trying to resolve is how can I write an OpenAPI spec that would describe these possibilities and support validating the output? It sounds like discriminators is not the proper way to describe this, but what options are available to accomplish this? Could you provide an example using the requests/responses I provided in the original post?
- Bookmark
- Subscribe
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
Much depends how opinionated you would like to be in the design and what your needs are for extensibility. Here are some options that you may find helpful.
The most common mechanism would be to define a query parameter which accepts an array of strings and to use the description and examples to provide appropriate guidance for the API consumer.
An example of that would be:
parameters:
- in: query
name: fields
description: You can use the _fields_ parameter to restrict the set of properties returned to only those you explicitly specify. The properties are specified as a comma-separated list. For example, to return only the `name` and `color` properties of pets, you would set the parameter like `fields=name,color`
schema:
type: array
uniqueItems: true
items:
type: string
style: form
explode: false
examples:
singleProperty:
summary: Return only the name of the Pet
value: [name]
multipleProperties:
summary: Return the Pet's name, age and color
value: [name, age, color]
You can also define the parameter as an array of string enums. Enums are generally regarded as complete (e.g. you would not extend overtime in case of breaking a consumer implementation). However as this would only ever be for a request parameter and assuming you follow the practice of never removing a property from your design (e.g. breaking change), then it could fit your needs. This option gives you the ability to assert the condition against the specification itself.
parameters:
- in: query
name: fields
description: You can use the _fields_ parameter to restrict the set of properties returned to only those you explicitly specify. The properties are specified as a comma-separated list. For example, to return only the `name` and `color` properties of pets, you would set the parameter like `fields=name,color`
schema:
type: array
uniqueItems: true
items:
type: string
style: form
explode: false
examples:
singleProperty:
summary: Return only the name of the Pet
value: [name]
multipleProperties:
summary: Return the Pet's name, age and color
value: [name, age, color]
You could also specify an enum as part of an anyOf to guide the consumer but leave it open for further extension (not as strict as the previous option).
parameters:
- name: attributes
in: query
schema:
type: array
items:
type: string
anyOf:
- enum: [name, color, age]
- {}
required: false
description: You can use the _attributes_ parameter to restrict the set of properties returned to only those you explicitly specify. The properties are specified as a comma-separated list. For example, to return only the `name` and `color` properties of pets, you would set the parameter like `fields=name,color`. The known values are defined by the following enumeration [`name `, `age`, `color`] but this may extend overtime and other values provided will be processed if possible
style: form
explode: false
examples:
singleProperty:
summary: Return only the name of the Pet
value: [name]
multipleProperties:
summary: Return the Pet's name, age and color
value: [name, age, color]
Regardless of the chosen option, you would have to implement the logic to parse the query parameter and use the values to construct a response representation based on the provided selections.
I'd recommend providing some examples in your responses to demonstrate a full response representation as well as some filtered examples.
Example
responses:
'200':
description: Get Pets response
content:
application/json:
schema:
$ref: '#/components/schemas/Pets'
examples:
default:
value:
- name: spot
color: black
age: 2
eyeColor: brown
nameOnly - only name provided in query:
value:
- name: spot
nameAndAge - two propertiese provided in query:
value:
- name: spot
age: 2
I hope this helps you make a choice for your implementation.
