Rails 4 render json with multiple objects and includes - ruby-on-rails

I have a Rails 4 API. When a user search in the view for boats, this method is executed getting all the boats matching the search filters and return an array of boat models as json using render ActiveModel and the :include and :only like this:
render :json => #boats, :include => { :mainPhoto => {:only => [:name, :mime]},
:year => {:only => :name},
# other includes...}
This is working great.
But, additional to this information, in the view, I would like to show the total count of boats like "showing 1 - 20 of 80 boats" because there is a pagination funcionality. So, the point is I need to provide the 80 boats. I would like to avoid send two requests that execute almost the same logic, so the idea is to run the searchBoats method just once and in the result provide the list of boats and the total number of boats in a variable numTotalBoats. I understand numTotalBoats is not a boat model attribute. So, I think it should go in an independent variable in the render result.
Something like:
render :json => {boats: #boats with all the includes, numTotalBoats: #NumTotalBoats}
I tried thousands of combinations, but or I´m getting syntax errors or none of them is returning the expected result, something like
{boats: [boat1 with all the includes, boat2 with all the includes, ... boatN with all the includes], numTotalBoats: N}

Without adding any gems:
def index
boats = Boat.includes(:year)
render json: {
boats: boats.as_json(include: { year: { only: :name } }),
numTotalBoats: boats.count
}
end
At some point though, I believe you should use stand-alone serializers:
Note: Depending on whether you're using pagination gem or not, you might need to change .count calls below to .total_count (for Kaminari) or something else that will read the count correctly from paginated collection.
I recommend using ActiveModel Serializers and this is how it would be accomplished for your case.
Start by adding the gem to Gemfile:
gem 'active_model_serializers', '~-> 0.10'
Overwrite the adapter in config/initializers/active_model_serializer.rb:
ActiveModelSerializers.config.adapter = :json
Define serializers for your models,
# app/serializers/boat_serializer.rb
class BoatSerializer < ActiveModel::Serializer
attributes :name
has_one :year
end
# app/serializers/year_serializer.rb
class YearSerializer < ActiveModel::Serializer
attributes :name
end
And finally, in your controller:
boats = Boat.includes(:year)
render json: boats, meta: boats.count, meta_key: "numTotalBoats"
And you will achieve:
{
"boats": [
{
"name": "Boaty McBoatFace",
"year": {
"name": "2018"
}
},
{
"name": "Titanic",
"year": {
"name": "1911"
}
}
],
"numTotalBoats": 2
}
Adding that count in each index controller is a bit tedious, so I usually end up defining my own adapters or collection serializers in order to take care of that automatically (Tested with Rails 5, not 4).
# lib/active_model_serializers/adapter/json_extended.rb
module ActiveModelSerializers
module Adapter
class JsonExtended < Json
def meta
if serializer.object.is_a?(ActiveRecord::Relation)
{ total_count: serializer.object.count }
end.to_h.merge(instance_options.fetch(:meta, {})).presence
end
end
end
end
# config/initializers/active_model_serializer.rb
ActiveModelSerializers.config.adapter = ActiveModelSerializers::Adapter::JsonExtended
# make sure to add lib to eager load paths
# config/application.rb
config.eager_load_paths << Rails.root.join("lib")
And now your index action can look like this
def index
boats = Boat.includes(:year)
render json: boats
end
And output:
{
"boats": [
{
"name": "Boaty McBoatFace",
"year": {
"name": "2018"
}
},
{
"name": "Titanic",
"year": {
"name": "1911"
}
}
],
"meta": {
"total_count": 2
}
}
I think it's a little easier to parse this count for different endpoints and you will get it automatically while responding with a collection, so your controllers will be a little simpler.

Related

Rails 5 ActiveRecord, how to include fields from associated tables?

With Rails 5, Given the models:
chef_positions
* id
* name
skills
* id
* name
chef_position_skills
* id
* chef_position_id
* skill_id
I have a controller method to return the chef_position_skills by chef_position_id:
def index
chef_position = ChefPosition.find(params[:chef_position_id])
render json: chef_position.chef_position_skills
end
This returns:
[{"id":1,"chef_position_id":2,"skill_id":1,"created_at":"2017-06-05T15:44:06.821Z","updated_at":"2017-06-05T15:44:06.821Z"},{"id":2,"chef_position_id":2,"skill_id":2,"created_at":"2017-06-05T15:44:06.821Z","updated_at":"2017-06-05T15:44:06.821Z"}]
How can I get the controller to do the following:
include skill.name for each record
Do not include the timestamps
you need to associate the two first if you haven't already, in your model, chef_positions.rb
has_many :skills, through: :chef_position_skills
Then in your controller,
ChefPosition.where(id: params[:chef_position_id]).joins(:skills).select('chef_positions.id, skills.name')
def index
chef_position = ChefPosition.find(params[:chef_position_id])
render json: chef_position.chef_position_skills.map(&:json_data)
end
# ChefPositionSkill#json_data
def json_data
to_json(
include: { skill: { only: [:name] } },
only: [:id, :chef_position_id, :skill_id]
)
end
Define a method json_data (just for convenience), and use .map to call it for each chef_position_skill.
The include and only are standard json serializer methods, which assist rails in what needs to be included.
The only drawback (as far as I see), is that now you will have another attribute "skill": { "name": "skill_name" } in your final json.
Use
render json: chef_position.chef_position_skills.
map {|s| s.slice(:id, :chef_position_id, :skill_id).merge s.skill.slice(:name) }
I don't have the same models but here is a similar example:
irb(main):026:0> u.slice(:id, :email).merge u.funds.slice(:min)
=> {"id"=>1, "email"=>"test#example.com", "min"=>1000000}
But I think you'll really like JBuilder which is included in Rails 5.
https://github.com/rails/jbuilder

Using Ransack in a Rails 5 API only application

I recently started working on a Rails 5 API only application and I included jsonapi-resources as well as ransack to easily filter the results for any given request. By default, jsonapi-resources provides basic CRUD functionality, but in order to insert the search parameters for ransack I need to overwrite the default index method in my controller:
class CarsController < JSONAPI::ResourceController
def index
#cars = Car.ransack(params[:q]).result
render json: #cars
end
end
Now, this works fine, but it no longer uses jsonapi-resources to generate the JSON output, which means the output changed from:
# ORIGINAL OUTPUT STRUCTURE
{"data": [
{
"id": "3881",
"type": "cars",
"links": {
"self": ...
},
"attributes": {
...
}
}]
}
To a default Rails JSON output:
[{
"id": "3881",
"attr_1": "some value",
"attr_2": "some other value"
}]
How can I keep the original output structure while patching the index method in this controller?
Try to use the gem https://github.com/tiagopog/jsonapi-utils. In your case the problem will be solved in this way:
class CarsController < JSONAPI::ResourceController
include JSONAPI::Utils
def index
#cars = Car.ransack(params[:q]).result
jsonapi_render json: #cars
end
end

Add method to json output with include in rails

I am getting info from my database
#teams = Team.select(:id,:name)
Then, I am rendering this info with an assosiation
render json: #teams, include: :players
This produce me the following input:
[
{
"id": 1,
"name": "Lyon Gaming",
"players": [
{
"name": "Jirall",
"position": "Superior",
},
{
"name": "Oddie",
"position": "Jungla",
}
]
}
]
How ever I need to sort the players in an specific order (not alphabetical)
I already have the sql statement to do it; how ever I dont know how to apply this method to each association.
I am looking something like:
render json: #teams, (include: :players, method: custom_order)
But this raise me an error.
Any ideas?
This should work:
render json: #teams, include: :players, methods: :custom_order
I'd like to add that if you will always want to return the object this way, you can add this to your model instead:
def as_json(options = nil)
super(include: :players, methods: :custom_order)
end
As Adam has mentioned, active_model_serializers is good to have if your render json's start getting ugly. If the complexity doesn't grow too much, I always recommend keeping the dependencies down.
https://github.com/rails-api/active_model_serializers
You need to read more about serializers in rails(This is good practice)
Here you have a nice article about this https://www.sitepoint.com/active-model-serializers-rails-and-json-oh-my/
Then you can make something like this
app/serializers/team_serializer.rb
class TeamSerializer < ActiveModel::Serializer
attributes :id, :name, :ordered_players
end
app/controllers/teams_controller.rb
def index
respond_with(Team.all.map(&TeamSerializer.method(:new)))
end

Rabl conforming to jsonapi.org

I'm having a lot of trouble getting rabl to conform the the jsonapi.org spec. I have seen the wiki entry on github regarding this (https://github.com/nesquena/rabl/wiki/Conforming-to-jsonapi.org-format) but I dont understand how to get multiple root level nodes in rabl and still have collections work. The wiki entry on collections says this should work
collection [#post] => :posts
attributes :id, :title
child #post => :links do
node(:author) { #post.author_id }
node(:comments) { #post.comments_ids }
end
which for single root documents it does, but as soon as I try to add a meta or links to the root of the document as declared in the jsonapi.org spec, those nodes are appended to the existing collection node. Here is my rabl_init.rb
require 'rabl'
Rabl.configure do |config|
config.cache_sources = Rails.env != 'development'
config.include_child_root = false
config.include_json_root = false
end
I would like json that looks like this:
{
"links": {
"posts.comments": "http://example.com/posts/{posts.id}/comments"
},
"posts": [{
"id": "1",
"title": "Rails is Omakase"
}, {
"id": "2",
"title": "The Parley Letter"
}]
}
is Rabl capable of doing this?
Try something like this:
object false
node(:links) do
{"posts.comments" => "http://example.com/posts/{posts.id}/comments"}
end
child(#posts) do
attributes :id, :title
end

Rails 3.2: Serialize a collection of ActiveRecord objects to JSON with a single root element

How do I get a collection of ActiveRecords to serialize to a JSON object with a single root element that contains an array of the colletions elements?
We just upgraded from Rails 3.0.10 to Rails 3.2 and running into problems with the way collections of ActiveRecord objects are serialized to JSON.
I've done some reading and know that using to_json the way we are is probably a bad idea and we'll look to fix that soon but in the interim, I'm looking for the fastest way way possible to fix my code so it does what it did before as these changes broke our API.
We have a collection ActiveRecord objects of type Form that were being returned in an "index" action as JSON using the following code:
def index # in our FormsController
# get our "records" collection from the database
respond_to do |format|
yield(format) if block_given?
# other formats excluded for simplicity
format.json {
records = records.call if records.is_a?(Proc)
render :json => records.to_json(serialize_records_options), :layout => false
}
end
end
# These are in ApplicationController
def serialize_options
(self.class.serialize_options if self.class.respond_to? :serialize_options) || {}
end
def serialize_records_options
options = serialize_options.clone
options[:root] = (options[:root].pluralize if options[:root]) || controller_name
options[:indent] ||= 2
options
end
The problem is that this used to serialize as:
{
"total_entries": 2,
"total_pages": 1,
"forms": [{
"form": {
... attributes ...
}
},
{
"form": {
... attributes ...
}
}],
"per_page": 5,
"current_page": 1
}
and now serializes as:
[{
"forms": {
... attributes of form 1 ...
}
},
{
"forms": {
... attributes of form 2 ...
}
},
{
... more forms ...
}]
Our client apps are not recognizing that format as even being a valid JSON object. Any ideas on how we can get it to output in the original format? We haven't changed any of our serialization code other than upgrading Rails and it's dependencies. We have the following JSON gems in our bundle:
$ bundle show | grep json
* json (1.7.3)
* jsonpath (0.5.0)
* multi_json (1.3.5)
Thanks in advance! This is a weird one.

Resources