Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* Your contribution here.

* [#108](https://github.com/ruby-grape/grape-active_model_serializers/pull/108): Document JSON:API included resources and verify relationship linkage, explicit inclusion, and collection deduplication through API requests - [@dblock](https://github.com/dblock).
* [#110](https://github.com/ruby-grape/grape-active_model_serializers/pull/110): Add SimpleCov LCOV reports, merged stable-matrix Coveralls reporting, and a coverage badge; remove the obsolete Code Climate badge and refresh gem/CI badge links - [@dblock](https://github.com/dblock).
* [#109](https://github.com/ruby-grape/grape-active_model_serializers/pull/109): Document the optional third-party Swagger model parser, response model configuration, and schema limitations - [@dblock](https://github.com/dblock).
* [#106](https://github.com/ruby-grape/grape-active_model_serializers/pull/106): Update default serializer options documentation to use AMS 0.10 field filtering and cover request-level defaults and overrides - [@dblock](https://github.com/dblock).
Expand Down
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- [API Versioning](#api-versioning)
- [Manually specifying serializer / adapter options](#manually-specifying-serializer--adapter-options)
- [Error Serialization](#error-serialization)
- [JSON:API Included Resources](#jsonapi-included-resources)
- [Custom Metadata](#custom-metadata)
- [Default Serializer Options](#default-serializer-options)
- [Current User](#current-user)
Expand Down Expand Up @@ -257,6 +258,50 @@ end

With the `:json_api` adapter, validation failures return HTTP 422 with a [JSON:API errors array](https://jsonapi.org/format/#error-objects). The response shape follows the configured ActiveModelSerializers adapter, not the Grape format name: the example still uses `format :json`. Setting the global adapter also affects successful responses.

### JSON:API Included Resources

A resource identifier under `relationships` is linkage, not the related resource's attributes. To include those attributes in a top-level `included` array, declare the relationship in the serializer and pass an AMS `include` option with the `:json_api` adapter:

```ruby
class AuthorSerializer < ActiveModel::Serializer
attributes :id, :name
end

class ArticleSerializer < ActiveModel::Serializer
attributes :id, :title
belongs_to :author, serializer: AuthorSerializer
end

get '/articles/:id' do
article = Article.find(params[:id])
render article, adapter: :json_api, include: 'author'
end
```

With the Grape AMS formatter configured and `format :json`, an article with an author produces a compound document such as:

```json
{
"data": {
"id": "1",
"type": "articles",
"attributes": { "title": "Hello" },
"relationships": {
"author": { "data": { "id": "2", "type": "authors" } }
}
},
"included": [
{ "id": "2", "type": "authors", "attributes": { "name": "Jane" } }
]
}
```

Related records need identifiers and serializers defining the attributes to expose. Without `include: 'author'`, the JSON:API adapter can emit relationship linkage without adding the author's full resource to `included`.

You can also specify `adapter: :json_api, include: 'author'` as route options, or return them from `default_serializer_options`. Nested relationships use paths such as `include: 'comments.author'`; each relationship must be declared in its serializer.

ActiveRecord's `includes(:author)` preloads database associations but does not select AMS included resources. Likewise, an `?include=author` query parameter is not automatically forwarded to AMS; validate allowed relationships and explicitly pass the selected include option. See the [AMS include documentation](https://github.com/rails-api/active_model_serializers/blob/0-10-stable/docs/general/adapters.md#include-option).

### Custom Metadata

```ruby
Expand Down
75 changes: 75 additions & 0 deletions spec/features/grape-active_model_serializers/render_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,78 @@ def get_resource_with(meta)
end
end
end

describe 'JSON:API included resources' do
let(:app) { Class.new(Grape::API) }
let(:render_options) { { adapter: :json_api } }
let(:route_options) { {} }
let(:collection) { false }
let(:document) { JSON.parse(last_response.body) }
let(:author_resource) {
{ 'id' => '2', 'type' => 'authors', 'attributes' => { 'name' => 'Jane' } }
}

before do
stub_const('JsonApiAuthor', Class.new(ActiveModelSerializers::Model) do
attributes :id, :name
end)
stub_const('JsonApiArticle', Class.new(ActiveModelSerializers::Model) do
attributes :id, :title, :author
end)
stub_const('JsonApiAuthorSerializer', Class.new(ActiveModel::Serializer) do
type 'authors'
attributes :id, :name
end)
stub_const('JsonApiArticleSerializer', Class.new(ActiveModel::Serializer) do
type 'articles'
attributes :id, :title
belongs_to :author, serializer: JsonApiAuthorSerializer
end)

ActiveModelSerializers.config.adapter = :json
app.format :json
app.formatter :json, Grape::Formatter::ActiveModelSerializers
author = JsonApiAuthor.new(id: 2, name: 'Jane')
article = JsonApiArticle.new(id: 1, title: 'Hello', author: author)
articles = [article, JsonApiArticle.new(id: 3, title: 'More', author: author)]
resource = collection ? articles : article
options = render_options
app.get('/articles', **route_options) { render resource, options }

get '/articles'
end

it 'emits relationship linkage without implicitly including the resource' do
expect(document.fetch('data').fetch('relationships')).to eq(
'author' => { 'data' => { 'id' => '2', 'type' => 'authors' } }
)
expect(document).not_to have_key('included')
end

context 'render inclusion' do
let(:render_options) { super().merge(include: 'author') }

it 'includes the related resource and its attributes' do
expect(document.fetch('included')).to eq([author_resource])
end

context 'a collection' do
let(:collection) { true }

it 'includes a shared related resource only once' do
ids = document.fetch('data').map { |article| article.fetch('id') }
expect(ids).to eq(%w[1 3])
expect(document.fetch('included')).to eq([author_resource])
end
end
end

context 'route options' do
let(:route_options) { { adapter: :json_api, include: 'author' } }
let(:render_options) { {} }

it 'selects the adapter and included resource without render overrides' do
expect(document.fetch('included')).to eq([author_resource])
end
end
end
Loading