Description
Intent
A number of issues have tried to address this in the past (#202, #205, #753, #536). Most of those issues were closed in favor of #795, and most Discourse threads on this topic eventually point to #795.
However, #795's scope involves two main points:
- Creating/updating a single resource with its relations in one go
- Multiple operations in a single request (primarily to support transactions)
This issue's scope is only the former. The intent is to resolve a common issue without trying to solve every other related issue at the same time. In other words, let's support something like accepts_nested_attributes_for in Rails.
Scope
As compared to #795:
- * It should allow mixed actions in a single request (e.g. creating + updating).
+ * It should allow creating/updating a resource and its relationships in a single request
* It should work for multiple types of resources (e.g. articles + tags) that would otherwise be accessed
at different endpoints.
* Every request should be transactional (i.e. all operations should succeed or fail together).
* Additive to the base spec (probably as an extension).
+ * Minimal footprint possible, favoring #795 for more advanced use cases
+ * Forwards-compatible with the larger scope of #795.
Here are a couple use-cases that test these requirements:
- * Create two articles, add one existing tag to both, create a new tag and add to both.
* Change the title of an article and update its tags: add one existing tag, create another,
and remove another (without replacing the whole tag set).
Example
In this form, we need to:
- Add already-persisted tags (say, from an autocomplete)
- Add unpersisted tags (based on user input)
- Remove tags (when editing)
- Submit the post and its tags at the same time, so validations can run together.
Proposal
- Embrace @beauby's proposal for dealing with partial updates to collections. TL;DR: introduce
added-data
,removed-data
, andupdated-data
keys for partial updates, keepingdata
for a full replacement of the collection. - Allow resource objects inside the relationship
data
field, not just resource identifiers (for instance, sendattributes
in the relationship'sdata
key).
This has been proposed in the past, most recently by @andruby (#795 (comment)). In fact, a number of users already assume this is part of the spec (#795 (comment), #795 (comment), #795 (comment), cerebris/jsonapi-resources#424 (comment), #795 (comment)).
As opposed to creating a post and assigning pre-existing tags (currently supported by the spec):
Scenario A Creation:
Create a post and its tags in a single request:
{
data: {
type: 'posts',
attributes: { title: 'my title' },
relationships: {
tags: {
data: [
- { id: 1, type: 'tags' },
+ { type: 'tags', attributes: { name: 'mytag' } },
]
}
}
}
}
Scenario B: Update:
Update the post's title, create a new tag, update an existing tag, and remove an existing tag.
NB: data
is still valid for replacing the entire collection.
{
data: {
type: 'posts',
attributes: { title: 'my title' },
relationships: {
tags: {
- data: [
- { id: 1, type: 'tags' },
- ]
+ updated-data: [
+ { id: 1, type: 'tags', attributes: { name: 'updated' } }
+ ],
+ added-data: [
+ { type: 'tags', attributes: { name: 'anothertag' } }
+ ],
+ removed-data: [
+ { id: 1, type: 'tags' }
+ ]
]
}
}
}
}
Scenario C Multi-level nesting:
Create a post with a tag, and the tag's creator, in a single request. The creator
here could have attributes specified as well. This can go N levels deep:
{
data: {
type: 'posts',
attributes: { title: 'my title' },
relationships: {
tags: {
data: [
{
id: 1,
type: 'tags',
+ relationships: {
+ creator: {
+ data: { id: 1, type: 'users' }
+ }
+ }
}
]
}
}
}
}
Scenario D Destruction:
This draws the distinction between disassociating a tag (see above) and deleting it altogether.
{
data: {
type: 'posts',
attributes: { title: 'my title' },
relationships: {
tags: {
+ deleted-data: [
+ { id: 1, type: 'tags' }
+ ]
}
}
}
}
Pros
- Minimal and intuitive. As noted above, many users assume this is already part of the spec.
- Solves a common use case.
- Does not require client-generated ids (temporary or otherwise)
Cons
-
Impossible to reference an embedded resource in more than one relationship. See @ethanresnick's example here.
I'd like to highlight this part:
Problems of this sort only become more common when this strategy is used in conjunction with the bulk extension. For example, suppose that I want to create multiple articles at once and tag both of them with the same new tag...this is impossible. Or, on a job board, I want to add multiple job postings at once and link them with the same new employer.
I agree these types of problems mostly come up when dealing with bulk operations. So I would suggest not solving this use case as part of this issue. If users have this issue, they can use a TBD operations extension currently in discussion in Support for multiple operations in a single request #795.
-
There are some concerns the premise of nested resources breaks REST principles. Per the description of Support for multiple operations in a single request #795, it does look like a goal of JSON API to support nested resources, but this is a valid issue. For a discussion of this area please see @brainwipe's original comment, then read down to this comment closing the discussion. If you view the concept of any form of nested resources as inherently a Bad Idea, please downvote this issue (and lend your feedback to Support for multiple operations in a single request #795 as well).
Alternatives
- Sideposting: It looks like this proposal by @ethanresnick was reaching general consensus before it was closed in favor of Support for multiple operations in a single request #795. I can get on board with something like this as well, though I favor the smaller footprint and more intuitive approach defined above.
- Operations: We could punt on this and fold it into Support for multiple operations in a single request #795, which has been the current approach. I would like to avoid this since embedded resources is an incredibly common requirement, operations less so. Embedded resources can be solved without biting off the additional complexity and RPC-style semantics.
Client-Side Ephemeral IDs
One problem with the above approach is getting a reference to the newly-created nested resource on the client. In the case of Ember, for instance, you'd be left with an in-memory record with isNew
set to true
, and no id
.
There are two ways to solve this:
- A The server response must sideload any nested relationship sent in the request. This way clients can drop the records that were just sent to the server and replace them with the contents of
included
. I have been using this pattern with Rails and ember-data without problems. - B Ephemeral client-side IDs. The client sends something like
temp-id: 'abc-123' }
as part of the record, the server responds with the same.
I believe A should be the default, and B should be an optional implementation left up to users to accommodate more advanced use cases. However I have no problems if the community reached a consensus that B was a better option. I suggest avoiding 'sideposting' as it makes client-side temporary ids required, whereas I'd prefer them optional.
(I will continue to update this proposal as consensus is reached in the comments. Last updated: September 21, 2016)