如何使用继承建模 RESTful API?

我有一个对象层次结构,我需要通过一个 RESTful API 公开,我不知道我的 URL 应该如何结构化,他们应该返回什么。我找不到任何最佳实践。

假设我有猫狗继承了动物的遗产。我需要对狗和猫进行 CRUD 操作; 我也希望能够对动物进行一般操作。

我的第一个想法是这样做:

GET /animals        # get all animals
POST /animals       # create a dog or cat
GET /animals/123    # get animal 123

问题在于/Animal 集合现在是“不一致的”,因为它可以返回并获取结构不完全相同的对象(狗和猫)。集合返回具有不同属性的对象是否被认为是“ RESTful”?

另一个解决方案是为每个具体类型创建一个 URL,如下所示:

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

但是现在猫和狗的关系已经消失了。如果要检索所有动物,必须查询狗和猫的资源。网址的数量也将随着每个新的动物亚型而增加。

另一项建议是通过增加以下内容来扩大第二种解决办法:

GET /animals    # get common attributes of all animals

在这种情况下,返回的动物将只包含所有动物共有的属性,删除特定于狗和特定于猫的属性。这允许检索所有的动物,虽然与较少的细节。每个返回的对象都可以包含到详细的具体版本的链接。

有什么意见或建议吗?

28803 次浏览

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.

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

But now the relationship between dogs and cats is lost.

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

This question can be better answered with the support of a recent enhancement introduced in the latest version of OpenAPI, v3 at time of writing.

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 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 (to choose one of the subtypes) and allOf (to combine 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 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?

Yes, you are wrong. Also relationships can be modelled following the OpenAPI specs e.g. in this polymorphic way.

Chicken:
type: object
discriminator:
propertyName: typeInformation
allOf:
- $ref:'#components/schemas/Chicken'
- type: object
properties:
eggs:
type: array
items:
$ref:'#/components/schemas/Egg'
name:
type: string

...