How to model a RESTful API with inheritance?
I have an object hierarchy I need to expose through a RESTful API and I'm not sure how my URLs should be structured and what they should return. I could not find any best practices.
Let's say I have Dogs and Cats inheriting from Animals. I need CRUD operations on dogs and cats; I also want to be able to do operations on animals in general.
My first idea was to do something like this:
GET /animals # get all animals
POST /animals # create a dog or cat
GET /animals/123 # get animal 123
The thing is that the /animals collection is now "inconsistent", as it can return and take objects that do not have exactly the same structure (dogs and cats). Is it considered "RESTful" to have a collection returning objects that have differing attributes?
Another solution would be to create an URL for each concrete type, like this:
GET /dogs # get all dogs
POST /dogs # create a dog
GET /dogs/123 # get dog 123
GET /cats # get all cats
POST /cats # create a cat
GET /cats/123 # get cat 123
But now the relationship between dogs and cats is lost. If one wishes to retrieve all animals, both the dog and cat resources must be queried. The number of URLs will also increase with each new animal subtype.
Another suggestion was to augment the second solution by adding this:
GET /animals # get common attributes of all animals
In this case, the animals returned would only contain attributes common to all animals, dropping dog-specific and cat-specific attributes. This allows to retrieve all animals, although with fewer details. Each returned object could contain a link to the detailed, concrete version.
Any comments or suggestions?
I would suggest:
- Using only one URI per resource
- Differentiating between animals solely at the attribute level
Setting up multiple URIs to the same resource is never a good idea because it can cause confusion and unexpected side effects. Given that, your single URI should be based on a generic scheme like /animals
.
The next challenge of dealing with the entire collection of dogs and cats at the "base" level is already solved by virtue of the /animals
URI approach.
The final challenge of dealing with specialized types like dogs and cats can be easily solved using a combination of query parameters and identification attributes within your media type. For example:
GET /animals
(Accept : application/vnd.vet-services.animals+json
)
{
"animals":[
{
"link":"/animals/3424",
"type":"dog",
"name":"Rex"
},
{
"link":"/animals/7829",
"type":"cat",
"name":"Mittens"
}
]
}
-
GET /animals
- gets all dogs and cats, would return both Rex and Mittens -
GET /animals?type=dog
- gets all dogs, would only return Rex -
GET /animals?type=cat
- gets all cats, would only Mittens
Then when creating or modifying animals, it would be incumbent on the caller to specify the type of animal involved:
Media Type: application/vnd.vet-services.animal+json
{
"type":"dog",
"name":"Fido"
}
The above payload could be sent with a POST
or PUT
request.
The above scheme gets you the basic similar characteristics as OO inheritance through REST, and with the ability to add further specializations (i.e. more animal types) without major surgery or any changes to your URI scheme.
This question can be better answered with the support of a recent enhancement introduced in the latest version of the OpenAPI.
It's been possible to combine schemas using keywords such as oneOf, allOf, anyOf and get a message payload validated since JSON schema v1.0.
https://spacetelescope.github.io/understanding-json-schema/reference/combining.html
However, in the OpenAPI (former Swagger), schemas composition has been enhanced by the keywords discriminator (v2.0+) and oneOf (v3.0+) to truly support polymorphism.
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaComposition
Your inheritance could be modeled using a combination of oneOf (for choosing one of the subtypes) and allOf (for combining the type and one of its subtypes). Below is a sample definition for the POST method.
paths:
/animals:
post:
requestBody:
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/Dog'
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Fish'
discriminator:
propertyName: animal_type
responses:
'201':
description: Created
components:
schemas:
Animal:
type: object
required:
- animal_type
- name
properties:
animal_type:
type: string
name:
type: string
discriminator:
property_name: animal_type
Dog:
allOf:
- $ref: "#/components/schemas/Animal"
- type: object
properties:
playsFetch:
type: string
Cat:
allOf:
- $ref: "#/components/schemas/Animal"
- type: object
properties:
likesToPurr:
type: string
Fish:
allOf:
- $ref: "#/components/schemas/Animal"
- type: object
properties:
water-type:
type: string
I would go for /animals returning a list of both dogs and fishes and what ever else:
<animals>
<animal type="dog">
<name>Fido</name>
<fur-color>White</fur-color>
</animal>
<animal type="fish">
<name>Wanda</name>
<water-type>Salt</water-type>
</animal>
</animals>
It should be easy to implement a similar JSON example.
Clients can always rely on the "name" element being there (a common attribute). But depending on the "type" attribute there will be other elements as part of the animal representation.
There is nothing inherently RESTful or unRESTful in returning such a list - REST does not prescribe any specific format for representing data. All it says is that data must have some representation and the format for that representation is identified by the media type (which in HTTP is the Content-Type header).
Think about your use cases - do you need to show a list of mixed animals? Well, then return a list of mixed animal data. Do you need a list of dogs only? Well, make such a list.
Whether you do /animals?type=dog or /dogs is irrelevant with respect to REST which does not prescribe any URL formats - that is left as an implementation detail outside the scope of REST. REST only states that resources should have identifiers - never mind what format.
You should add some hyper media linking to get closer to a RESTful API. For instance by adding references to the animal details:
<animals>
<animal type="dog" href="/animals/123">
<name>Fido</name>
<fur-color>White</fur-color>
</animal>
<animal type="fish" href="/animals/321">
<name>Wanda</name>
<water-type>Salt</water-type>
</animal>
</animals>
By adding hyper media linking you reduce client/server coupling - in the above case you take the burden of URL construction away from the client and let the server decide how to construct URLs (which it by definition is the only authority of).