Relationships within abstract relationship


#1

Imagine the situation that you have parking with relationship cars.
Cars relationship is an array of resources of types sedan or truck.

It’s works perfectly when you need to get parking with all the cars it contains.

GET /parkings/{parking_id}?include=cars

But what to do when you need to get descendant relationships of sedans & trucks resources. For sedans you need trunk and for trucks you need trailer.

GET /parkings/{parking_id}?include=cars.trunk,cars.trailer

sedan has trunk but don’t has trailer and vice versa for truck. Isn’t it wrong and do we need to avoid abstractions when related resources they has different relationships?

GET /parkings/{parking_id}?include=sedans.trunk,trucks.trailer

I understand that sometimes abstract resources could have abstract descendant relationship names, but there are cases when only one resource has relationship of name X and others don’t have anything similar to it.

Thanks in advance!


#2

@michaelhibay sorry for calling you again, but your advices are best :}


#3

Haha, alright I’ve been busy doing conference work and starting a new job but I’ll see what I can do here to help. :smiley:

I’m not sure if this is the API you’re working with or a contrived example, but the problem here looks like you want the cars relationship to be polymorphic, and then utilize a feature of {json:api} which doesn’t support it.

If you look here, you’ll see some discussion from a while back where I make a bunch of points about resources not being objects.

The data shaping capabilities of {json:api} are not polymorphic, and are shaped by expected structure of the relationship hierarchy. This is OK and a really good design, as it is a motivator to keep the design simple and think about the design from another perspective.

Consider this, if you have a polymorphic ambiguous payload being returned to the consumer client, how much duplication of business logic would you expect that client to carry out? An ideal application would create a model completely reactive to the data returned, and by including this ambiguity you would require your clients to tightly couple themselves to your data model to simultaneously support all response messages for your resource model.

However, if the calling agent were to filter and explicitly understand the resource representation it was going to receive, it would be much easier to make changes to any particular representation or its client side handling without the addition of cyclomatic complexity.

My suggestion would be to refactor this in some way to allow for data shaping when filtering, or break it up into multiple requests. The other option if you control the client and the consumer, is to remember you control the default shape of the data so you could simply /parkings/{parking_id}?filter[type]=cars,trucks to get whatever shape you want. You could make this work within spec, but I think it really exposes an underlying flaw in the API design. Resources are not objects.

I hope this helps!


#4

Thank you for the answer, @michaelhibay! I’m hope you are doing well with your new job.

This is contrived example, for easier understanding without subject area specific vocabulary.

You are right, I’m talking about polymorphic collection which will return resources of different types, but I understand that each type could have different relations in it (they are differ, thats why they have different types). I understand that resource !== object.

By providing example /parkings/{parking_id}?filter[type]=cars,trucks I suppose you meant /parkings/{parking_id}?filter[cars][type]=sedans,trucks. Because it’s not a parking for trucks only or sedans only. It’s parking, which could contain sedans as well as trucks. Parking could have it’s own types: open, underground, rooftop. And if we need to get all open parkings with cars of type sedan and truck only:
/parkings/{parking_id}?filter[type]=open&filter[cars][type]=sedan,truck.

But all this stuff isn’t solving original issue I’ve tried to explain here. If I understand you right, we should avoid to have collection of resources which will return resources of different types if their descendant relations differ from each other.

In other words:

  • If resource1 has descendant relations child1 and child2 and resource2 has the same relations they could be collected into polymorphic-relation.
  • If descendant relations are differs - resource1 and resource2 shouldn’t be collected into polymorphic-relation.
  • If descendant relations are differs and resource1 and resource2 were collected into polymorphic-relation we should make extra requests to collect all relations for each item of this collection.

Did I understand you right?


#5

Thanks, so far so good!

Ok, that certainly gives me some more perspective to understand the difficulty you are talking about.

The issue I was speaking about is the data shaping of a non-uniform set of message representations. The spec is very lenient to provide you the capability to offer this type of functionality, however the more customized you get the less likely the behavior of consumers would be across the ecosystem. If you control both, you can be a bit more custom but you need to project the definition of this behavior explicitly and completely.

The ability to construct this kind of data modeling is stretching the capabilities of the simple relationship traversal mechanism. In addition to the ability to determine the default included resources for any resource, you also have the option to include the deep nested relationship links to include higher order relationships without traversal of the long chains. In your example this could be something like cars-features, where the features are the trunk / trailer …etc.

One final note, since this is a contrived example I wasn’t trying to pick apart the example itself but perhaps this could give you an idea. The definition of cars and trucks as different resources is something which is complicating the solution space, because there is similar resources which are differentiated by something other than the query parameters.

/parkings/{parking_id}?include=vehicles,vehicles.features&filter[vehicles.type]=(sedan,truck)&filter[vehicles.features.type]=(trunk,trailer)

The include parameter is expected to operate on a uniform set of resources, so what you can do is make the set uniform by containing the dynamic nature of the resource representations to a uniform pattern. Pass include parameter to include the features resource as well, and send only the relevant features by filtering.

Apologies if this example is a bit rushed and not completely vetted or confusing, but I’m out of time and my family is eager to begin the holiday celebrations! I hope something in here helps!


#6

Thank you for diving deep and providing such detailed overview.

Thats very interesting idea to extract all uncommon relationships within collection to new abstract relationship and then filter it out by types you need.
For example if all the types MUST have wheels it could be kept as separate relationship, but all the rest will be collected to features relationship. Or even keep all the parts of the vehicle inside this features relationship.

The only thing I don’t like about this solution that frontend client will be required to have 2 separate transformers for truck resource which was received from /trucks?include=trailer and one more for truck resource which were received from polymorphic relation /parkings?include=vehicles,vehicles.features. And the same for each resource type which could be located in polymorphic collection.

At the beginning I’ve thought that you offer to make something like virtual ?include=vehicle.features parameter which will be transformed to something like ?include=sedan.trunk,truck.trailer under the hood. In this case structure of sedan & truck resources will be consistent in any time, but this breaks json:api spec as I understand.


#7

The main issue with this way in my opinion - inconsistent resource structure. By the way this isn’t problem, it’s just possibility of the problem in the future. I cannot be 100% sure that it’s right or wrong, just leaving it here as a notice that more research required.

GET /trucks?include=trailer output:

{
  "data": [{
    "type": "truck",
    "id": "vehicle-1",
    "relationships": {
      "trailer": {
        "data": {
          "type": "trailer",
          "id": "vehicle-1-trailer"
        }
      }
    }
  }]
}

GET /parkings?include=vehicles,vehicles.features

{
  "data": [{
    "type": "truck",
    "id": "vehicle-1",
    "relationships": {
      "features": {
        "data": {
          "type": "trailer",
          "id": "vehicle-1-trailer"
        }
      }
    }
  }]
}

#8

Why?

This could be open ended and asked in both directions, why does the structure have to be different from the two? I don’t think that is a good idea. Why is there a /truck resource? Shouldn’t it be /vehicle?filter[type]=truck, or is there some other reason? If the filter defines the truck resource well enough, why would the front end need a transformer in any other shape than the one?

Remember, resources are not objects and complex types are composed of other types so you have the latitude to define the composition format which makes most sense while serving the needs of the consumer.


#9

I thought that creation of the trucks will be in POST /trucks endpoint, to have more precise relations (do not drop everything to features relation, but have many relations). Otherwise POST /vehicles endpoint handler will be very complex and hard to maintain if there will be a lot of types of vehicles, each type has it’s own collection of relations one of them required, and other don’t.


#10

Remember that {json:api} documents are typed, so a simple switch statement or dynamic handler can easily allow each type processing to be separately developed and executed based on the type attribute in the root of the compound document. No additional complexity is introduced, the URL processor is simply behaving differently for a different message.

APIs are not CRUD, your payload does not need to be in the same format all the time at the same URL. You will always need to manage the ingestion of each message type, the only difference is that it isn’t at the /truck endpoint, instead it is at a common abstracted resource URL like /vehicle or /asdfnailsdilfasdf.


#11

I’ve returned back to this question while designed Conversations system.

GET /conversations could return different types of conversations. Each conversation type scoped by different scope types.

Candidate has an interview for a Job. It’s JobInterview conversation:

POST /conversations
{
  "data": {
    "type": "JobInterview",
    "relationships": {
      "scopes": {
        "data": [
          {
            "type": "Job",
            "id": "job-1"
          },
          {
            "type": "Candidate",
            "id": "candidate-1"
          }
        ]
      }
    }
  }
}

As you can see both of scopes are located in polymorphic scopes relationship. It works well when you want to add another type of conversation which requires another scope types. For example DisputeNegotiation conversation will require scopes of type Dispute and maybe NegotiationSession if you want to break them down daily.

It’s easy to collect the data because GET /conversations will return all the types of the conversations and you could filter them out. But isn’t that hard for a client to understand what scopes are required for concrete type of conversation? I don’t even know how to describe in documentation and error messages that conversation of type JobInterview requires 2 scopes where first one MUST be Job and the second one MUST be Candidate. And conversation of type DisputeNegotiation requires 2 scopes where first one MUST be Dispute and the second one MUST be NegotiationSession. Moreover there is possibility of conversations with more or less than 2 scopes.

All this things makes me feel wrong. Wouldn’t it be clearer for the client to have this API:

POST /conversations
{
  "data": {
    "type": "JobInterview",
    "relationships": {
      "job": {
        "data": {
          "type": "Job",
          "id": "job-1"
        }
      },
      "candidate" {
        "data": {
          "type": "Candidate",
          "id": "candidate-1"
        }
      }
    }
  }
}

Then if client want to create DisputeNegotiation he will have another 2 relationships: dispute and session. Then documentation and error messages will be clear and human friendly. But then we are returning to the issue which I described earlier. While getting resources from endpoint GET /conversations each resource type will have different relationships.

After all I’ve thought about resource creation could use concrete relations (eg. job, candidate, dispute and etc) and resource fetching could use concrete or abstract relationship scopes for the filtering purposes. Under the hood all this concrete relations will store as scopes. That will give client an opportunity to chose what way to use. But it’s not obvious behavior that modifying scopes will affect on concrete relationships. Then I suppose it would be better to make scopes relationship read only.

Isn’t it an overkill?


#12

I think the level of down the rabbit hole on the contrived example drowned out some of the more basic things I was saying, namely that the design needs to be simple to understand for the client. Not knowing the real application of your question meant I had to answer the question you were asking as best as I could guess.

With this second example, I’m getting the impression you are looking for a way to do a compound identifier within a polymorphic collection. I really don’t think the format supports (well) this kind of functionality. I think you would be better served spending your time fixing the resource model to not require this kind of processing, than to discover a way to make the format support it.


#13

I’ve thought about creating resource with type Scope, but it wouldn’t help much, because under the hood it will have same issue, it will require set of different relationships for different use cases.

I think that {json:api} specification not completely fit polymorphic relations at this moment and this topic should be discovered more. In my case there are a lot of endpoints where polymorphism will be possible in future.


#14

The format is certainly not optimized for all use cases, and it is very likely you are correct. However, I’d caution you to not throw the baby out with the bathwater. The value we add in the API space is in abstracting a lot of this complexity to form a more simple uniform interface for consumers.

If this is a recurrent problem you are encountering than I think one of these two things is true:

  1. {json:api} does not support your application and domain constraints well.
  2. The way the services are designed can be optimized to better fit the model.

Either way, good luck I hope you find your answer!