Rails define custom json structure - ruby-on-rails

I'm attempting to structure a json response to mimic an existing structure we have elsewhere in our application (using jbuilder templates). In this specific use case, we are unable to use the jbuilder template because we are preparing the json data for a live update job being fired from a model method, not responding to a server call in the controller.
Desired structure:
{"notes": {
"1": {
"id": "1",
"name": "Jane",
...
},
"2": {
"id": "2",
"name": "Joe",
...
}
}
}
Jbuilder Template (for reference):
json.notes do
for note in notes
json.set! note.id.to_s do
json.id note.id.to_s
json.name note.name
...
end
end
end
I've tried defining a to_builder method in the model class (below), per the jbuilder docs, and active model serializers but can't seem to get the hashes nested under the id attribute. Any direction would be appreciated!
to_builder method
def to_builder
Jbuilder.new do |note|
note.set! self.id do
note.(self, :id, :name, ...)
end
end
end

How about just plain Ruby?
notes.each_with_object({"notes": {}}) do |note, hash|
hash["notes"][note.id.to_s] = note.as_json
end
Or AMS:
adapter = ActiveModelSerializers::Adapter
notes.each_with_object({"notes": {}}) do |note, hash|
hash["notes"][note.id.to_s] = adapter.create(NoteSerializer.new(note))
end

Related

RSpec check of correct serialization - result wraps serialized in an array

I want to check if response from my endpoint is serialized correctly (I'm using Fast JSON API serializer). Here is my code:
endpoint
get do
::Journeys::JourneyListSerializer.new(Journey.where(is_deleted: false))
end
JourneyListSerializer:
module Journeys
class JourneyListSerializer
include FastJsonapi::ObjectSerializer
attribute :content do |object|
object.content_basic.dig('entity')
end
end
end
my specs:
let!(:journey) { create(:journey, :with_activities, content_basic: hash) }
let(:hash) {{ "environment": "master", "entity_type": "item", "event_type": "update", "entity": { "id": "5202043", "type": "item", "attributes": { "title": "New text" }}}}
it 'serializes journey with proper serializer' do
call
expect(JSON.parse(response.body)).to be_serialization_of(journey, with: ::Journeys::JourneyListSerializer)
end
be_serialization_of helper in support/matchers
def be_serialization_of(object, options)
serializer = options.delete(:with)
raise 'you need to pass a serializer' if serializer.nil?
serialized = serializer.new(object, options).serializable_hash
match(serialized.as_json.deep_symbolize_keys)
end
Whole specs gives me an error:
1) API::V1::Journeys::Index when params are valid serializes journey with proper serializer
Failure/Error: expect(JSON.parse(response.body)).to be_serialization_of(journey, with: ::Journeys::JourneyListSerializer)
expected {"data"=>[{"attributes"=>{"content"=>{"attributes"=>{"title"=>"New text"}, "id"=>"5202043", "type"=>"item"}}, "id"=>"75", "type"=>"journey_list"}]} to match {:data=>{:id=>"75", :type=>"journey_list", :attributes=>{:content=>{:id=>"5202043", :type=>"item", :attributes=>{:title=>"New text"}}}}}
Diff:
## -1,2 +1,2 ##
-:data => {:attributes=>{:content=>{:attributes=>{:title=>"New text"}, :id=>"5202043", :type=>"item"}}, :id=>"75", :type=>"journey_list"},
+"data" => [{"attributes"=>{"content"=>{"attributes"=>{"title"=>"New text"}, "id"=>"5202043", "type"=>"item"}}, "id"=>"75", "type"=>"journey_list"}],
I don't know why the result is in array, how to avoid that? when I'm using this endpoint it serialized without array.
You're testing two different types of input here. The controller is testing a list of Journey but your test is serializing just one. Your matcher is fine, you are just putting the wrong input.
get do
// "where" always returns an array of journeys
::Journeys::JourneyListSerializer.new(Journey.where(is_deleted: false))
end
// "journey" in the test is just one object, so you need to pass an array in the test
expect(JSON.parse(response.body)).to be_serialization_of([journey], with: ::Journeys::JourneyListSerializer)

Combining multiple objects in an array for json using Grape

I'm using the grape gem in my rails application. When returning a single record I can use the grape active model serializer. When returning multiple objects I run into an undefined error. How should I structure my multiple campaign records?
app/controllers/api/v1/campaigns.rb
Returning Multiple records
resource :campaigns do
desc "Return all campaigns"
get "", root: :campaigns do
#campaign_array = []
#campaigns = Campaign.all
#campaigns.each do |campaign|
#campaign_array << {
backers: campaign.orders.count,
funded: campaign.orders.completed.sum(:quantity),
campaign: campaign
}
end
#campaign_array
end
end
Output - Error
undefined method `read_attribute_for_serialization'
Returning a single record
desc "Return a campaign"
get ":id", root: :campaign do
#campaign = Campaign.where(id: permitted_params[:id]).first!
{
backers: #campaign.orders.count,
funded: #campaign.orders.completed.sum(:quantity),
campaign: #campaign
}
end
Output
{
"backers": 1,
"funded": 2,
"campaign": {
"id": 5,
"min_funding_goal_units": 20,
"product_name": "Another product",
"product_description": "A product description",
}
}
It's look like you return an array which is not serialized.
Does your index works if you just return #campaigns ? (just to be sure that the problem come from the serialization)
Maybe can you try to «help» Grape by indicating format :json below get "", root: :campaigns do
Let me know.
You need to use each_serializer instead of serializer when it's an
array of items
There is an issue in github about this problem. Related issue.

Rails 4 render json with multiple objects and includes

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.

How to change default behavior of empty Jbuilder partials?

Model relation: Article belongs_to Author
Sample jbuilder view:
json.extract! article,
:id,
:created_at,
:updated_at
json.author article.author, partial: 'author', as: :author
What happens when Article has no Author:
{
"id": 1,
"created_at": "01-01-1970",
"updated_at": "01-01-1970",
"author": []
}
Question:
Is there a clean way to force jbuilder to display null or {} when variable passed to associated template is empty? This problem is prevalent across quite big application and adding code like that article.author.empty? ? json.author(nil) : json.author(article.author, partial: 'author', as: :author) everywhere is not something I'd want to do. Perhaps some form of helper that wouldn't require too much refactoring?
I don't want to override core jbuilder functionality as I don't want to break it (partials accepting multiple variables for example).
Related jbuilder issue: https://github.com/rails/jbuilder/issues/350
This will accomplish what you want
json.author do
if article.author.blank?
json.null!
else
json.partial! 'authors/author', author: article.author
end
end
I would suggest a helper though to avoid all the duplication:
module ApplicationHelper
def json_partial_or_null(json, name:, local:, object:, partial:)
json.set! name do
object.blank? ? json.null! : json.partial!(partial, local => object)
end
end
end
Then you would call it like this:
json_partial_or_null(json, name: 'author', local: :author, object: article.author, partial: 'authors/author')

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

Resources