Skip to content

Conflicting behavior for Relationships when using fields and/or include #124

Open
@Diasporism

Description

@Diasporism

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

  1. The attempt to be extra helpful with non-standard meta information in the relationships section when no relationships were asked for
  2. This line passing fields instead of include into the requested_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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions