Description
I've noticed there is a non-standard behavior (feature?) that's breaking the json api spec when including different combinations of fields
and include
parameters.
For example, given three models Author
, Book
, and Article
where an author has many books as well as many articles and both books and articles belong to an author:
if you request /authors/1
you will get a payload that looks like:
{
"data": {
"id": 1,
"type": "authors",
"attributes": {
"name": "Neal Stephenson"
},
"relationships": {
"books": {
"meta": {
"included": false
}
},
"articles": {
"meta": {
"included": false
}
}
}
}
The fact that "relationships"
is present when I didn't request anything in the include
param is a little weird, but I guess it's helpful? More on this in a bit...
Next hitting /authors/1?include=books
outputs something like:
{
"data": {
"id": 1,
"type": "authors",
"attributes": {
"name": "Neal Stephenson"
},
"relationships": {
"books": {
"data": [
{
"type": "books",
"id": 1
},
{
"type": "books",
"id": 2
}
]
}
}
},
"included": [
{
"id": 1,
"type": "books",
"attributes": {
"name": "Snow Crash"
}
},
{
"id": 2,
"type": "books",
"attributes": {
"name": "Cryptonomicon"
}
}
]
}
At first glance that seems pretty much as expected, and actually in my opinion this is correct. But what's weird is that the relationships
node no longer includes:
"articles": {
"meta": {
"included": false
}
}
meta
is a non-standard, anything goes kinda field regardless, and relationships
may or may not exist at any given time according to the spec so I guess this might be technically correct, but it's not what I would consider to be good consistency.
But it gets worse:
Go ahead and request /authors/1?include=books&fields[books]=name
and the linkage between relationships
and included
is straight up broken:
{
"data": {
"id": 1,
"type": "authors",
"attributes": {
"name": "Neal Stephenson"
}
},
"included": [
{
"id": 1,
"type": "books",
"attributes": {
"name": "Snow Crash"
}
},
{
"id": 2,
"type": "books",
"attributes": {
"name": "Cryptonomicon"
}
}
]
}
You might say "Oh but you can just infer the books belong to the main resource," except the same thing happens on the index action as well (/authors?include=books&fields[books]=name
):
{
"data": [
{
"id": 1,
"type": "authors",
"attributes": {
"name": "Neal Stephenson"
}
},
{
"id": 2,
"type": "authors",
"attributes": {
"name": "R.A. Salvatore"
}
}
],
"included": [
{
"id": 1,
"type": "books",
"attributes": {
"name": "Snow Crash"
}
},
{
"id": 2,
"type": "books",
"attributes": {
"name": "Cryptonomicon"
}
},
{
"id": 3,
"type": "books",
"attributes": {
"name": "The Legend of Drizzt"
}
}
]
}
That ain't gunna work for anybody.
Anyway, I've been able to break this using various combinations of include
and fields
params and it all seems to boil down to one culprit (which is a combination of two conflicting ideas):
- The attempt to be extra helpful with non-standard meta information in the
relationships
section when no relationships were asked for - This line passing
fields
instead ofinclude
into therequested_relationships
method.
A naive fix is to modify two methods in lib/jsonapi/serializable/resource.rb
rels = requested_relationships(fields).each_with_object({}) do |(k, v), h|
h[k] = v.as_jsonapi(include.include?(k))
end
to
rels = requested_relationships(include).each_with_object({}) do |(k, v), h|
h[k] = v.as_jsonapi(include.include?(k))
end
and
def requested_relationships(fields)
@_relationships.select { |k, _| fields.nil? || fields.include?(k) }
end
to
def requested_relationships(includes)
return {} if includes.empty?
@_relationships.select { |k, _| includes.include?(k) }
end
This has one caveat: relationships
is no longer returned unless a user requested one or more via the include
param (which in turn breaks a bunch of specs). However, this is still technically correct according to spec.
So what do you say? If I put in a PR to fix it can we live without
"relationships": {
"books": {
"meta": {
"included": false
}
},
"articles": {
"meta": {
"included": false
}
}
}
which really wasn't helping anyone anyway?