Managing relationships with data

Hi all!
Our team is going to implement some APIs. We committed to use json:api specification, but we struggle to handle correctly the relationship concept.
We have Project and Components. Each Project contains an ordered set of Components. Each Component could be contained in several Projects. A relationship between the two entities is used to store the position of each Component within a Project. We have identified the following resources:

  • Project (a resource)
  • Component (a resource)
  • ProjectComponent (a relationship resource): this resource is the only way we have found (in our intepretation of json:api) to handle the position of a Component within a Project

I would like to know if the following content is correct in terms of json:api specification.

Project

{
    links: {
        self: "/project/1"
    },
    data: [{
        id: "1",
        type: "project",
        attributes: {
            title: "Project A"
        },
        relationships: {
            project_components: {
                links: {
                    self: "/project/1/relationships/project_components",
                    related: "/project/1/project_components"
                },
                data: [{
                    id: "A",
                    type: "ProjectComponent"
                },
                {
                    id: "B",
                    type: "ProjectComponent"
                }]
            }
        }
    }]
}

Relationship ProjectComponents

{
    links: {
        self: "/project/1/relationships/project_components",
        related: "/project/1/project_components"
    },
    data: [{
        id: "A",
        type: "ProjectComponent"
    },
    {
        id: "B",
        type: "ProjectComponent"
    }]
}

ProjectComponents

{
    links: {
        self: "/project/1/project_components"
    },
    data: [{
        id: "A",
        type: "ProjectComponent",
        attributes: {
            position: "1"
        },
        relationships: {
            project: {
                links: {
                    related: "/project/1"
                },
                data: [{
                    id: "1",
                    type: "project"
                }]
            },
            component: {
                links: {
                    related: "/component/A"
                },
                data: [{
                    id: "A",
                    type: "component"
                }]
            }
        }
    {
        id: "B",
        type: "ProjectComponent",
        attributes: {
            position: "2"
        },
        relationships: {
            project: {
                links: {
                    related: "/project/1"
                },
                data: [{
                    id: "1",
                    type: "project"
                }]
            },
            component: {
                links: {
                    related: "/component/B"
                },
                data: [{
                    id: "B",
                    type: "component"
                }]
            }
        }
    }]
}

Component

{
    links: {
        self: "/components"
    },
    data: [{
        id: "A",
        type: "Component",
        attributes: {
            title: "Component A"
        },
        relationships: {
            project_components: {
                links: {
                    related: "/project/1/project_components/A"
                },
                data: [{
                    id: "A",
                    type: "ProjectComponent"
                }]
            },
            component_questions: {
                links: {
                    self: "/component_questions/A"
                },
                data: [{
                    id: "100",
                    type: "ComponentQuestion"
                },{
                    id: "101",
                    type: "ComponentQuestion"
                }]
            }
        }
    {
        id: "B",
        type: "Component",
        attributes: {
            title: "Component B"
        },
        relationships: {
            project_components: {
                links: {
                    related: "/project/1/project_components/B"
                },
                data: [{
                    id: "B",
                    type: "ProjectComponent"
                }]
            },
            component_questions: {
                links: {
                    self: "/component_questions/B"
                },
                data: [{
                    id: "200",
                    type: "ComponentQuestion"
                },{
                    id: "201",
                    type: "ComponentQuestion"
                }]
            }
        }
    }]
}

Yes. Everything looks fine.

This is one way to model an ordered relationship.

As relationship data may be interpreted as an ordered list you could omit the interim resource. But this has the trade-off that changing the order requires a full replacement of the relationship. This may cause collisions.

Your approach using an index also comes with trade-offs. Moving a resource in the list may require updating several Indexes at once. You have two options doing so:

  1. Updating only the index of the moved resource. The server takes care of updating all other indexes to ensures that there aren’t any collisions. And - depending on your implementation - no gaps. The server needs to inform the client about those side-effects by including the changes resources in the response.

  2. Updating the index of all affected resources. This has the major trade-off that an extension such as Atomic Operations is required. JSON:API base specification does not support updating multiple resources in an atomic operation.

@jelhan
Thank you for your answer.

Ok. I should say, we were a bit worried, because to model a simple relationship that carries one value (the position) we had to define several endpoints. But this is how the json:api specification works, hence it’s ok. It also means that we’ve correctly interpreted the specifications.

Yes, I’ve also thought about integrating the position within the order of the relationship itself, but I wanted to model a relationship in a way that could be extended with other attributes (even if now it was just to understand if the model was correctly defined).

About the consequent tradeoffs, we are going to update the position within the server. That means, a patch would trigger on the server a set of updates on different components. I’ve a question about the suggested response: if I patch a single resource, how could I send back in the response the resources that have been touched too? I send back all the list of resources when I patch one? Or at least the ones that have changed? This should be documented in the API.

I’ve read about that extension, at the moment we are not going to use it.

Thank you again.

You don’t need to expose such endpoints. The JSON:API specification does not enforce having the endpoints. It only defines processing rules for them if they exist. You can also have resources which are only available through including in a compound document.

A server can and should respond with a compound document in that case. If you include all related resources or only those which have been modified is an optimization question for your implementation.

Yes, this I know. But as soon as I need to modify them, I need at least some of them. In my case, I need at least the ProjectComponents because it’s the only way I have to edit the whole content of the relationship (if it carries more information than just the position). Because, if I’m not wrong, I can’t edit a related resource within a patch request. Or I use the AtomicOperation extension, but I would try to use it only when I can’t do otherwise.

Yes. Even to modify the position you need to provide a self link for that resource. If you have a self link it must support fetching the resource via GET request as well.

Wait, I’m a big confused now.
In Project we have the relationship project_components:

...
relationships: {
            project_components: {
                links: {
                    self: "/project/1/relationships/project_components",
                    related: "/project/1/project_components"
                },
                data: [{
                    id: "A",
                    type: "ProjectComponent"
                },
                {
                    id: "B",
                    type: "ProjectComponent"
                }]
            }
        }
... 

that provides two links.

  • self, to obtain the same list of object (relationship), and
  • related, that provides a way to obtain the related resource, ProjectComponents.

I thought I could avoid self, because what it returns is just this:

links: {
    self: "/project/1/relationships/project_components",
    related: "/project/1/project_components"
},
data: [{
    id: "A",
    type: "ProjectComponent"
},
{
    id: "B",
    type: "ProjectComponent"
}]

While I must have the related because in my situation it’s the only way to manipulate the content of the relationship.

{
    links: {
        self: "/project/1/project_components"
    },
    data: [{
        id: "A",
        type: "ProjectComponent",
        attributes: {
            position: "1"
        },
...

With self here

Do you mean this? Right? Self refers to the resource object itself, not the relationship defined in project.

If yes, then I could say that I’ve understood correctly the specification.

I was talking about the self link of a resource. That’s different from the self link of a relationship.

The self link of a resource is used to interact with that resource. E.g. to fetch it (GET), to update it (PATCH), or to delete it (DELETE).

The self link of a relationship is used to interact with the relationship. E.g. to add a resource to a has-many relationship (POST) and to remove a resource from a has-many relationship (DELETE).

Depending on your API design you may not need neither self nor related links for the project_components relationship. You could provide relationship data only via compound document from either the project (GET /project/1?include=project_components.component) or the component (GET /component/A?include=project_components.project). The relationship between a project and component could be managed by creating (POST /project/1/project_components) or deleting (DELETE /project/1/project_components/A) the interim model.

That’s a much better and clear explanation of what I was meaning with my post.

Thank you very much for your help! I would say that for me the whole question got an answer that is satisfying.