Rails ActiveModel::Serializer nest response in "data": parent - ruby-on-rails

I have a rails app in which I use the gem active_model_serializers. In my responses I would like to nest my results inside a "data": parent. Currently when I don't get any data for a response I get the following JSON:
[]
What I want is something like this:
{
"data": []
}
I would also like to use the same format in cases where I have data, like this:
{
"data": [
{
"id": 135,
[...]
I've managed to get the structure I want by using render json, like this:
render json: { data: respond_values}
But in this case my serialiser gets ignored and all the attributes in my model gets returned. My serialiser looks like this:
class TranslationSerializer < ActiveModel::Serializer
attributes :id, :value, :created_at, :updated_at, :language_id
has_one :language
has_one :localized_string, serializer: LocalizedStringParentSerializer
end
If I instead use respond_with my serialiser works but I don't get the structure I want - the data parent / container is missing.
Any ideas on what I need to to to get my serialiser to work properly?

First off unless you need to support a legacy API use the JSON:API adapter:
By default ActiveModelSerializers will use the Attributes Adapter (no
JSON root). But we strongly advise you to use JsonApi Adapter, which
follows 1.0 of the format specified in jsonapi.org/format.
While nobody fully agrees with all the design decisions in JSON:API it is widely supported by front-end frameworks such as Ember and Angular and is likely to gain further traction.
Otherwise you would need to create your own adapter since the JSON adapter does not allow you to set the root key.
# lib/active_model_serializers/adapters/bikeshed_adapter.rb
module ActiveModelSerializers
module Adapters
class BikeshedAdapter < Json
def root
:data
end
end
end
end
ActiveModelSerializers.config.adapter = :bikeshed

For any reason, Rails is not finding a Serializer which matches to the model. Maybe something is missing in the convention name/namespace of your model with serializer.
https://github.com/rails-api/active_model_serializers/blob/master/docs/general/rendering.md
But, if you explicit declare the serializer, it should work.
render json: #post, serializer: PostPreviewSerializer

Related

ActiveModel::Serializer Subclass that will accept any attribute dynamically?

I'm building an API engine for an existing app, which will serve JSON with ActiveModel::Serializer. On the existing app, There are some controllers that just render regular old hashes that aren't instances of any ActiveModel subclass - originally, these were AJAX endpoints so it didn't matter what class the response body was.
I need to recreate some of these existing endpoints in the API module, so for instances like these, I want to build a custom serializer that will accept whatever attributes you throw at it. something like...
In the Controller:
def show
response = {
key: "this is a custom object and not an AM instance"
}
render json: response, serializer: Api::V1::CustomSerializer
end
And the serializer:
module Api
module V1
class CustomSerializer < ActiveModel::Serializer
def attributes
*object.keys.map(&:to_sym)
end
def read_attribute_for_serialization(attr)
object[attr.to_s]
end
end
end
end
Couple of problems:
a) The call to render in the controller doesn't seem to like the amount of args I've passed to render, which supposedly takes *args, which suggests to me there is something wrong with the override methods I've written.
b) If I were to just put attributes *object.class.column_names.map(&:to_sym) in the first line of the class, object is undefined outside of a method.
c) I call it inside a method, the resulting hash is nested inside whatever I choose to call that method. Not really what I had in mind.
My question: has anyone successfully created a serializer that will accept any attribute? Would love to know how.
PLEASE NOTE: I would like achieve this with AMS if I can - we're using the adapter for JSON API for all our response bodies. I would much rather make this work then initialize a hash identical to the json api standard we're using every time the response is not an AM instance.
For the folks who might have stumbled across the same problem, I ended up assembling a catch-all serializer class for anything i want to render that isn't a subclass of Active Record. Like so:
module Api
module V1
class CustomSerializer
def initialize(obj, error: false, type: nil)
#hash = error ? error_hash(obj) : success_hash(type, obj)
end
def to_h
#hash
end
private
def error_hash(obj)
{
errors: {
pointer: obj[:error] || obj[:errors]
},
detail: detail(obj)
}
end
def success_hash(type, obj)
{
id: obj.try(:id) ? obj[:id] : nil,
type: type.nil? ? 'hash' : type,
data: obj.try(:id) ? obj.except(:id) : obj,
links: ''
}
end
def detail(obj)
obj[:detail] || obj[:message] || obj[:msg]
end
end
end
end
Note that I'm working with the JSON API standard. Then, instead of doing something like this for an activemodel serializer:
render json: #device, serializer: Api::V1::DeviceSerializer
I can do something like this:
render json: Api::V1::CustomSerializer.new(#response, error: false, type: 'feed').to_h
basically just means I can still render the JSON api standard for anything that is just an instance of a Hash class, or anything for which I'm doing external API requests and storing in a Hash. Hope this helps someone someday.

ActiveModel::Serializer not allowing me to load serializer in app/serializers

I have the following api controller in ('app/controllers/api/v1/companies_controller.rb'):
class Api::V1::CompaniesController < ApplicationController
def index
companies = Company.all
render json: companies, serializer: CompanySerializer
end
end
but the above is looking for Api::V1::CompanySerializer in app/serializers/api/v1/company_serializer.rb. I'd rather just have a generic company serializer in app/serializers/company_serializer. I have tried using the scope resolution operator like:
render json: companies, serializer: ::CompanySerializer
but still getting an error. How would I tell my controller to use the default serializer explicitly? I have seen this issue https://github.com/rails-api/active_model_serializers/issues/1701 and, if this is the ONLY behavior allowed, it seems like a strange choice
AMS 0.10.7 Implicit Serializer vs Explicit Serializer
You really don't need to pass any serializer option as long as your serializer has the default name. So render json: companies should work.
Explicit way would look like:
render json: companies,
serializer: CollectionSerializer,
each_serializer: CompanySerializer
Note: The default serializer for collections is CollectionSerializer. In your example, you tried to pass CompanySerializer to serializer.

Dynamically include associations for objects in Rails

I am currently developing a small Rails 5 application where I need to pass on an ActiveRecord object to an external service based on certain events. In my model I have defined the following:
# /models/user.rb
after_create :notify_external_service_of_user_creation
def notify_external_service_of_user_creation
EventHandler.new(
event_kind: :create_user,
content: self
)
end
The EventHandler is then converting this object to JSON and is sending it through an HTTP request to the external service. By calling .to_json on the object this renders a JSON output which would look something like this:
{
"id":1234,
"email":"test#testemail.dk",
"first_name":"Thomas",
"last_name":"Anderson",
"association_id":12,
"another_association_id":356
}
Now, I need a way to include all first level associations directly into this, instead of just showing the foreign_key. So the construct I am looking for would be something like this:
{
"id":1234,
"email":"test#testemail.dk",
"first_name":"Thomas",
"last_name":"Anderson",
"association_id":{
"attr1":"some_data",
"attr2":"another_value"
},
"another_association_id":{
"attr1":"some_data",
"attr2":"another_value"
},
}
My first idea was to reflect upon the Model like so: object.class.name.constantize.reflect_on_all_associations.map(&:name), where object is an instance of a user in this case, and use this list to loop over the associations and include them in the output. This seems rather tedious though, so I was wondering if there would be a better way of achieving this using Ruby 2.4 and Rails 5.
If you don't want to use an external serializer, you can override as_json for each model. as_json gets called by to_json.
module JsonWithAssociations
def as_json
json_hash = super
self.class.reflect_on_all_associations.map(&:name).each do |assoc_name|
assoc_hash = if send(assoc_name).respond_to?(:first)
send(assoc_name).try(:map, &:as_json) || []
else
send(assoc_name).as_json
end
json_hash.merge!(assoc_name.to_s => assoc_hash)
end
json_hash
end
end
You'll need to prepend this particular module so that it overrides the default as_json method.
User.prepend(JsonWithAssociations)
or
class User
prepend JsonWithAssociations
end

Rails Scope issue with ActiveRecord::Serializer

I'm kind of new to rails and I just discovered serializers.
I proceed with the implementation for one of my model. I generated a serializer with
rails g serializer MyModel
Then in my controller, in my function that renders my data I do this :
render json: MyModel.where(...), each_serializer: MyModelSerializer, root: false
# note : that's the only line in my function
And finally my serializer :
class MyModelSerializer < ActiveModel::Serializer
attributes :id, :name, ... # all attributes in my model, I double-checked
end
The error i now get from the rails server is the following :
uninitialized constant MyModelController::MyModelSerializer
Am I missing something ? :(
Thanks for your help :)
By the way, I'll add that the function that is supposed to render json with the serializer is called with an ajax request from the view. Don't know if it changes things though...
Try to change this bit in your controller code:
each_serializer: ::MyModelSerializer
Two colon basically forces Ruby interpreter to look for MyModelSerializer class in the "root" and not within MyModelController. From your error message, that seems to be the problem.

JSON data for rspec tests

I'm creating an API that accepts JSON data and I want to provide testing data for it.
Is there anything similar to factories for JSON data? I would like to have the same data available in an object and in JSON, so that I can check if import works as I intended.
JSON has strictly defined structure so I can't call FactoryGirl(:record).to_json.
In cases like this, I'll create fixture files for the JSON I want to import. Something like this can work:
json = JSON.parse(File.read("fixtures/valid_customer.json"))
customer = ImportsData.import(json)
customer.name.should eq(json["customer"]["name"])
I haven't seen something where you could use FactoryGirl to set attributes, then get it into JSON and import it. You'd likely need to create a mapper that will take your Customer object and render it in JSON, then import it.
Following Jesse's advice, in Rails 5 now you could use file_fixture (docs)
I just use a little helper for reading my json fixtures:
def json_data(filename:)
file_content = file_fixture("#{filename}.json").read
JSON.parse(file_content, symbolize_names: true)
end
Actually you can do the following with factorygirl.
factory :json_data, class: OpenStruct do
//fields
end
FactoryGirl.create(:json_data).marshal_dump.to_json
Sometime ago we implemented FactoryJSON gem that addresses this issue. It worked quite well for us so far. Readme file covers possible use cases.
Here's something that works well for me. I want to create deeply nested structures without specifying individual factories for each nesting. My usecase is stubbing external apis with webmock. Fixtures don't cut it for me since I need to stub in a variety of different data.
Define the following base factory and support code:
FactoryBot.define do
factory :json, class: OpenStruct do
skip_create # Don't try to persist the object
end
end
class JsonStrategy < FactoryBot::Strategy::Create
def result(evaluation)
super.json.to_json
end
def to_sym
:json
end
end
# Makes the function FactoryBot.json available,
# which automatically returns the hash as a json string.
FactoryBot.register_strategy(:json, JsonStrategy)
I can then define the actual factory like this:
FactoryBot.define do
factory :json_response, parent: :json do
# You can define any attributes you want here because it uses OpenStruct
ids { [] }
# This attribute will be plucked by the custom strategy. All others like
# ids above will be ignored. You can still use them here though.
json do
ids.map do |id|
{
score: 90,
data: {
id: id,
},
}
end
end
end
end
Finally you can use it like this:
FactoryBot.json(:json_response, ids: [1,2])
=> "[{\"score\":90,\"data\":{\"id\":1}},{\"score\":90,\"data\":{\"id\":2}}]"

Resources