Rails: Serializing deeply nested associations with active_model_serializers
Solution 1
So this my not be the best or even a good answer, but this is working how I need it to.
While including nested and side-loaded attributes appears to be supported when using the json_api
adapter with AMS, I needed support for flat json. In addition, this method worked well because each serializer is specifically generating exactly what I need it to independent of any other serializer and without having to do anything in the controller.
Comments / alternate methods are always welcome.
Project Model
class Project < ActiveRecord::Base
has_many :estimates, autosave: true, dependent: :destroy
end
ProjectsController
def index
@projects = Project.all
render json: @projects
end
ProjectSerializer
class ProjectSerializer < ActiveModel::Serializer
attributes :id,
:name,
:updated_at,
# has_many
:estimates
def estimates
customized_estimates = []
object.estimates.each do |estimate|
# Assign object attributes (returns a hash)
# ===========================================================
custom_estimate = estimate.attributes
# Custom nested and side-loaded attributes
# ===========================================================
# belongs_to
custom_estimate[:project] = estimate.project.slice(:id, :name) # get only :id and :name for the project
custom_estimate[:project_code] = estimate.project_code
custom_estimate[:tax_type] = estimate.tax_type
# has_many w/only specified attributes
custom_estimate[:proposals] = estimate.proposals.collect{|proposal| proposal.slice(:id, :name, :updated_at)}
# ===========================================================
customized_estimates.push(custom_estimate)
end
return customized_estimates
end
end
Result
[
{
"id": 1,
"name": "123 Park Ave.",
"updated_at": "2015-08-09T02:36:23.950Z",
"estimates": [
{
"id": 1,
"name": "E1",
"release_version": "v1.0",
"exchange_rate": "0.0",
"created_at": "2015-08-12T04:23:38.183Z",
"updated_at": "2015-08-12T04:23:38.183Z",
"project": {
"id": 1,
"name": "123 Park Ave."
},
"project_code": {
"id": 8,
"valuation": 30,
"created_at": "2015-08-09T18:02:42.079Z",
"updated_at": "2015-08-09T18:02:42.079Z"
},
"tax_type": {
"id": 1,
"name": "No Tax",
"created_at": "2015-08-09T18:02:42.079Z",
"updated_at": "2015-08-09T18:02:42.079Z"
},
"proposals": [
{
"id": 1,
"name": "P1",
"updated_at": "2015-08-12T04:23:38.183Z"
},
{
"id": 2,
"name": "P2",
"updated_at": "2015-10-12T04:23:38.183Z"
}
]
}
]
}
]
I basically disregarded trying to implement any has_many
or belongs_to
associations in the serializers and just customized the behavior. I used slice
to select specific attributes. Hopefully a more elegant solution will be forth coming.
Solution 2
Per commit 1426: https://github.com/rails-api/active_model_serializers/pull/1426 - and related discussion, you can see that the default nesting for json
and attributes
serialization is one level.
If you want deep nesting by default, you can set a configuration property in an active_model_serializer initializer:
ActiveModelSerializers.config.default_includes = '**'
For detailed reference from v0.10.6: https://github.com/rails-api/active_model_serializers/blob/v0.10.6/docs/general/adapters.md#include-option
Solution 3
If you are using the JSONAPI adapter you can do the following to render nested relationships:
render json: @project, include: ['estimates', 'estimates.project_code', 'estimates.tax_type', 'estimates.proposals']
You can read more from the jsonapi documentation:http://jsonapi.org/format/#fetching-includes
Solution 4
You can change default_includes
for the ActiveModel::Serializer
:
# config/initializers/active_model_serializer.rb
ActiveModel::Serializer.config.default_includes = '**' # (default '*')
In addition, in order to avoid infinite recursion, you can control the nested serialization follows:
class UserSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :id, :phone_number, :links, :current_team_id
# Using serializer from app/serializers/profile_serializer.rb
has_one :profile
# Using serializer described below:
# UserSerializer::TeamSerializer
has_many :teams
def links
{
self: user_path(object.id),
api: api_v1_user_path(id: object.id, format: :json)
}
end
def current_team_id
object.teams&.first&.id
end
class TeamSerializer < ActiveModel::Serializer
attributes :id, :name, :image_url, :user_id
# Using serializer described below:
# UserSerializer::TeamSerializer::GameSerializer
has_many :games
class GameSerializer < ActiveModel::Serializer
attributes :id, :kind, :address, :date_at
# Using serializer from app/serializers/gamers_serializer.rb
has_many :gamers
end
end
end
Result:
{
"user":{
"id":1,
"phone_number":"79202700000",
"links":{
"self":"/users/1",
"api":"/api/v1/users/1.json"
},
"current_team_id":1,
"profile":{
"id":1,
"name":"Alexander Kalinichev",
"username":"Blackchestnut",
"birthday_on":"1982-11-19",
"avatar_url":null
},
"teams":[
{
"id":1,
"name":"Agile Season",
"image_url":null,
"user_id":1,
"games":[
{
"id":13,
"kind":"training",
"address":"",
"date_at":"2016-12-21T10:05:00.000Z",
"gamers":[
{
"id":17,
"user_id":1,
"game_id":13,
"line":1,
"created_at":"2016-11-21T10:05:54.653Z",
"updated_at":"2016-11-21T10:05:54.653Z"
}
]
}
]
}
]
}
}
Solution 5
In my case I created a file called 'active_model_serializer.rb' placed at 'MyApp/config/initializers' with the following content:
ActiveModelSerializers.config.default_includes = '**'
Do not forget to restart the server:
$ rails s
Eric Norcross
Developer with over ten years of professional experience. Detail orientated, end-user focused, and a passion for solving complex problems. Experienced in front & back-end development, system architecture, UX design, ad trafficking – oh, and a background in graphic design
Updated on August 03, 2022Comments
-
Eric Norcross almost 2 years
I'm using
Rails 4.2.1
andactive_model_serializers 0.10.0.rc2
I'm new to API's and chose
active_model_serializers
because it seems to be becoming the standard for rails (Although I'm not opposed to usingRABL
or another serializer)The problem I'm having is that I can't seem to include various attributes in multi-level relationships. For instance, I have:
Projects
class ProjectSerializer < ActiveModel::Serializer attributes :id, :name, :updated_at has_many :estimates, include_nested_associations: true end
and Estimates
class EstimateSerializer < ActiveModel::Serializer attributes :id, :name, :release_version, :exchange_rate, :updated_at, :project_id, :project_code_id, :tax_type_id belongs_to :project belongs_to :project_code belongs_to :tax_type has_many :proposals end
Proposals
class ProposalSerializer < ActiveModel::Serializer attributes :id, :name, :updated_at, :estimate_id belongs_to :estimate end
When I hit the
/projects/1
the above produces:{ "id": 1, "name": "123 Park Ave.", "updated_at": "2015-08-09T02:36:23.950Z", "estimates": [ { "id": 1, "name": "E1", "release_version": "v1.0", "exchange_rate": "0.0", "updated_at": "2015-08-12T04:23:38.183Z", "project_id": 1, "project_code_id": 8, "tax_type_id": 1 } ] }
However, what I'd like it to produce is:
{ "id": 1, "name": "123 Park Ave.", "updated_at": "2015-08-09T02:36:23.950Z", "estimates": [ { "id": 1, "name": "E1", "release_version": "v1.0", "exchange_rate": "0.0", "updated_at": "2015-08-12T04:23:38.183Z", "project": { "id": 1, "name": "123 Park Ave." }, "project_code": { "id": 8, "valuation": 30 }, "tax_type": { "id": 1, "name": "no-tax" }, "proposals": [ { "id": 1, "name": "P1", "updated_at": "2015-08-12T04:23:38.183Z" }, { "id": 2, "name": "P2", "updated_at": "2015-10-12T04:23:38.183Z" } ] } ] }
Ideally, I'd also like to be able to specify which attributes, associations, and attributes of those associations are included in each serializer.
I've been looking through the AMS issues, and there does seem to be some back and forth on how this should be handled (or if this kind of functionality is even actually supported) but I'm having difficulty figuring out exactly what the current state is.
- https://github.com/rails-api/active_model_serializers/issues/835
- https://github.com/rails-api/active_model_serializers/issues/968
- https://github.com/rails-api/active_model_serializers/issues/414
- https://github.com/rails-api/active_model_serializers/issues/444
One of the proposed solutions was to override the attribute with a method to call the nested attributes, but that seems to be regarded as a hack so I wanted to avoid it if possible.
Anyway, an example of what of how to go about this or general API advice would be much appreciated.
-
Eric Norcross over 8 yearsDoes this approach actually utilize
active_model_serializers
or is it just the default rails way of outputtingJSON
? -
Eric Norcross over 8 yearsI guess my biggest issue with this method is it requires managing the code in two different files; the Controller and the Serializer. That's not to say it isn't the correct way of doing it; just that I find it more inconvenient.
-
Mirror318 over 7 yearsWhat if you don't want deep nesting as the default, but you want it for one specific model? (And what if that specific model needs 2 or 3 levels of nesting?)
-
BF4 over 7 yearsyou can pass in
'**'
per action. Note, that it's a security issue. Better to be explicit what you want to include. -
BF4 over 7 years@greetification No, it uses a vestige of AMS that was left in Rails when it was reverted. See in Rails ActiveModel::Serializers::JSON and ActiveModel::Serialization
-
BF4 over 7 yearsThis is the best way to get 'nesting'.. though JSON:API actually flattens it.
-
Todd about 7 yearsWhile the most upvoted answer to this question is certainly the correct conventional way, its hard to get what you want when following AMS conventions. I too just write my own callbacks for displaying nested associations, and find the AMS convention too unwieldy. I find this to be the case for pretty much anything concerning Rails and nested associations - conventions are unwieldy, defer to plain ol' ruby.