How to model a RESTful API with inheritance?

27,074

Solution 1

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.

Solution 2

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

Solution 3

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).

Solution 4

But now the relationship between dogs and cats is lost.

Indeed, but keep in mind that URI simply never reflects relations between objects.

Solution 5

I know this is an old question, but I'm interested in investigating further issues on a RESTful inheritance modeling

I can always say that a dog is an animal and hen too, but hen makes eggs while dog is a mammal, so it can't. An API like

GET animals/:animalID/eggs

is not consistent because indicates that all subtypes of animal can have eggs (as a consequence of Liskov substitution). There would be a fallback if all mammals respond with '0' to this request, but what if I also enable a POST method? Should I be afraid that tomorrow there would be dog eggs in my crepes?

The only way to handle these scenarios is to provide a 'super-resource' which aggregates all the subresources shared among all possibile 'derived-resource' and then a specialization for each derived-resource that needs it, just like when we downcast an object into oop

GET /animals/:animalID/sons GET /hens/:animalID/eggs POST /hens/:animalID/eggs

The drawback, here, is that someone could pass a dog Id to reference an instance of hens collection, but the dog is not an hen, so it would not be incorrect if the response was 404 or 400 with a reason message

Am I wrong?

Share:
27,074
Alpha Hydrae
Author by

Alpha Hydrae

Updated on April 01, 2021

Comments

  • Alpha Hydrae
    Alpha Hydrae about 3 years

    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?

  • trcarden
    trcarden over 10 years
    This seems very similar to "casting" via a REST API. It also reminds me of the issues/solutions in the memory layout of a C++ subclass. For example where and how to both represent simultaneously a base and subclass with a single address in memory.
  • dipold
    dipold about 10 years
    I suggest: GET /animals - gets all dogs and cats GET /animals/dogs - gets all dogs GET /animals/cats - gets all cats
  • BrianT.
    BrianT. over 9 years
    In addition to specifying the desired type as a GET request parameter: it seems to me you could use accept type to achieve this too. That is: GET /animals Accept application/vnd.vet-services.animal.dog+json
  • LB2
    LB2 almost 8 years
    What about if cat and dog each has unique properties? How would you handle that on POST operation, as most frameworks wouldn't know how to properly deserialize it into a model as json doesn't carry good typing info. How would you handle post cases e.g. [{"type":"dog","name":"Fido","playsFetch":true},{"type":"cat‌​","name":"Sparkles",‌​"likesToPurr":"somet‌​imes"}?
  • mdw7326
    mdw7326 over 6 years
    @LB2 This is 120% a limitation of some frameworks. I think in this case you would need to define a pseudo "Model" that is defined to accept all your possible POST parameters. Then, do additional validation per the actual object type I.e. check that dogs.likesToPurr isn't defined. In a loosely structured framework like Flask you could check the type attribute first, then marshall the object depending on the specific type.
  • LB2
    LB2 over 6 years
    @mdw7326 the more proper way would be to use custom model binder, but that can be very painful to write one correctly.
  • emft
    emft over 6 years
    It's true that OAS allows this. However, there is no support for the feature to be displayed in Swagger UI (link), and I think a feature is of limited use if you can't show it to anyone.
  • Andrejs Cainikovs
    Andrejs Cainikovs over 6 years
    @emft, not true. As of writing this answer, Swagger UI already supports that.
  • Ryan.Bartsch
    Ryan.Bartsch almost 5 years
    What if dogs and cats had (majority) different properties? e.g.#1 POSTing a Communication for SMS (to, mask) vs. an Email (email address, cc, bcc, to, from, isHtml), or e.g.#2 POSTing a FundingSource for CreditCard (maskedPan, nameOnCard, Expiry) vs. a BankAccount (bsb, accountNumber)... would you still use a single API resource? This would seem to violate single responsibility from SOLID principles, but not sure if this applies to API design...
  • Andreas Gaus
    Andreas Gaus over 4 years
    Additional comment: focusing the API route GET chicken/eggs should also work using the common OpenAPI code generators for the controllers, but I did no check this yet. Maybe someone can try?
  • wired_in
    wired_in about 4 years
    I think you are placing too much emphasis on the URI structure. The only way you should be able to get to "animals/:animalID/eggs" is through HATEOAS. So you would first request the animal via "animals/:animalID" and then for those animals that can have eggs, there will be a link to "animals/:animalID/eggs", and for those that don't, there will not be a link to get from animal to eggs. If someone somehow ends up at eggs for an animal that can't have eggs, return the appropriate HTTP status code (not found or forbidden for instance)
  • Jethro
    Jethro over 3 years
    Thanks, this works great! It does seem currently true that the Swagger UI does not show this fully though. The Models will show in the Schemas section at the bottom, and any responses section referencing the oneOf section will partially show in the UI (schema only, no examples), but you get no example body for the request input. The github issue for this has been open for 3 years so likely to stay that way: github.com/swagger-api/swagger-ui/issues/3803
  • mae
    mae about 3 years
    If your response also returned the class name, what would the GET /animals/3424 request return? Would it say Animal or Dog?