Attribute is not rendered in json answer - ruby-on-rails

I'm changing existing serializer in ruby on rails(4.2) project adding new custom attribute to serializer(test123), which is not in model. But this attribute is not rendered in json answer which is formed via this serializer. Also i can change values of existing attributes (like author_name), which is in model.
Model contains of id and author_name.
Json is formed in controller of a class which has_many Examples.
My serializer:
class ExampleSerializer < ActiveModel::Serializer
attributes :id,
:author_name,
:test123
def test123
Rails.logger.debug("???!!!")
"test"
end
def author_name
"test"
end
end
Part of my controller:
def show
render json: #system,
include: %w[examples and other included data],
adapter: :json_api_secured,
each_serializer: detect_serializer
end
Server calls test123 (i see it by print ???!!! in logs), but in formed answer there is no field test123.
What can be a problem?

Related

API Rails: How to render a model's custom attribute in a controller?

In a #index action in my controller, I'm rendering the model as below. Though, I would like to include a custom attribute but it's never rendered, how can I do that?
class MyModel < ApplicationRecord
def custom_attr
attr1 + attr2
end
end
class MyModelsController < ApplicationController
def index
# 'custom-attr' is not rendered.
render json: MyModel.all, status: :ok, only: %i[attr1, attr2, custom_attr]
end
end
Thats not an attribute. Its just an instance method. attributes are really a Rails specific feature of models that is really a setter/getter coupled with metadata.
class Person
include ActiveModel::Model
include ActiveModel::Attributes
attribute :name
attribute :age
end
irb(main):009:0> Person.new.attributes
=> {"name"=>nil, "age"=>nil, "birthplace"=>nil}
When you render an model as JSON rails use #as_json which calls #serializeable_hash on the model. This serializes the attributes based on the attributes method. This is where the options are actually passed.
As #custom_attr is not actually an attribute its of course not included in the serialization.
You can solve this by:
Override #as_json on the model to customize its serialization.
Use a serializer layer such as ActiveModel::Serializers or JBuilder to customize the JSON representation of the model. (recommended)

Rails - How get first record of associated table?

i am trying to create an api for my mobile app.
I have posts and images tables. For my api, i can send all posts with:
#posts = Post.all
render json: #posts
Output: [{"id":20,"title":"Title 1", "body":" first post ", "user_id":1 }]
But it does not contain images at all. In order to show a showcase image in homepage of my app, i just need the first image of associated images.
The output which i need is (the name of showcase_image attribute does not matter) :
Output: [{"id":20, "title":"Title 1", "body":" first post ", "showcase_image": 'first_image.jpg' , "user_id":1 }]
I need to include first image from associated images table to my json response..
Thanks in advance !
I would suggest using a serializer. Active Model Serializer is pretty standard and easy to use, but is not receiving any updates and has a bad performance. You can choose any other serializer (I recommend Blueprinter) or use the AMS.
Through the AMS you coudl define the relation you want to serialize and it would build the json you're expecting
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body, :showcase_image, :user_id
def showcase_image
object.images.first.name # don't know what is the attribute you're looking for
end
end
And on your controller:
#posts = Post.includes(:images).all # Use includes to avoid N+1 problems
render json: #posts, serialize_collection: PostSerializer
You can include associations with the :include option when calling as_json.
render json: #posts.as_json(include: :images)
You could limit this to one image by adding a new association to Post.
class Post < ApplicationRecord
has_many :images
has_one :showcase_image, class_name: 'Image'
end
This would allow you to use the :showcase_image instead.
render json: #posts.as_json(include: :showcase_image)
You could also use Jbuilder to solve the issue at hand without adding an additional association.
# app/views/posts/index.json.jbuilder
# Get images that belong to posts, group them by post_id and
# return the minimum image id for each post_id.
images = Images.where(post_id: #posts.select(:id)).group(:post_id).minimum(:id)
# Request the full image data for all image ids returned above.
images = images.keys.zip(Image.find(images.values)).to_h
json.array! #posts do |post|
json.extract! post, :id, :title, :body, :...
json.showcase_image do
image = images[post.id]
if image
json.extract! image, :id, :name, :location, :...
else
json.null!
end
end
end
Without calling a specific render, Rails will default to the app/views/posts/index file, and select the file matching the request. (If you request HTML it will look for an HTML file, if you request JSON it looks for JSON, etc.)
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
#posts = Post.all
end
end
Now when you request /posts.json or /posts with the header Accept: application/json your application should return the JSON response build by Jbuilder.

Embeds_many child property not persisted when saving parent

I've been looking for some days without finding the exact answer to my problem which is as simple as that : I have a simple model, with books and authors. A book embeds many authors, and an author is embedded in book. But whenever I'm saving a new book, the author array is not persisted.
What I have is an angular 7 application, calling a ROR API. My Rails versions is 5.2.2. I am using mongoid 7.0 for persistence.
My API was generated with rails g scaffold, and with the --api and --skip-active-record flags.
I first had a problem with the mapping of my properties. My Angular APP sends JSON in lowerCamelCase, when Rails awaits form lower_snake_case vars. I managed to bypass this problem by adding a middleware (correct me if I'm wrong on this one) in my initializers which converts camelCase to snake_case.
# Transform JSON request param keys from JSON-conventional camelCase to
# Rails-conventional snake_case:
ActionDispatch::Request.parameter_parsers[:json] = -> (raw_post) {
# Modified from action_dispatch/http/parameters.rb
data = ActiveSupport::JSON.decode(raw_post)
data = {:_json => data} unless data.is_a?(Hash)
# Transform camelCase param keys to snake_case:
data.deep_transform_keys!(&:underscore)
}
From what I found looking for my problem, it could have been a problem with strong params, so I tried to get awat with this in my book_params
def book_params
#params.fetch(:book, {})
params.require(:book).permit(:title, :release_date, authors_attributes: [:name, :last_name, :birth_date])
end
These are my model :
class Person
include Mongoid::Document
field :last_name, type: String
field :first_name, type: String
field :birth_date, type: Date
end
class Author < Person
include Mongoid::Document
embedded_in :book
end
class Book
include Mongoid::Document
field :title, type: String
field :release_date, type: Date
embeds_many :authors
accepts_nested_attributes_for :authors
end
And this is POST in my book controller (generated with Rails)
# POST /books
def create
#book = Book.new(book_params)
if #book.save
render json: #book, status: :created, location: #book
else
render json: #book.errors, status: :unprocessable_entity
end
end
And here are exemple of a body sent, received and how it is processed by Rails :
Request sent by angular app
Request received and processed by Rails
We can see in the book object
"book"=>{"title"=>"azerty", "release_date"=>"2019-01-21T16:10:19.515Z"}}
That the authors have disappeared, though they were present in the request received by the server.
My question is then : what is the solution to this, or at least what am I missing ? Doesn't Mongoid automatically save children when using embedded documents and accepts_nested_attributes_for ? Should I manually save the children each time a parent is saved in my controller ?
Thanks in advance for helping me
You have to use nested attributes to save children records
Add following line in book model
accepts_nested_attributes_for :authors
And pass authors parameters in author_attributes, for exa:
{title: 'test', release_date: '', author_attributes: [{first_name: '', other_attributes of author}, {first_name: '', , other_attributes of author}]}
for more details please check Mongoid: Nested attributes
Pass perameters in this format
{"title"=>"test", "release_date"=>"2019-01-22", "book"=>{"title"=>"test", "release_date"=>"2019-01-22", "authors_attributes"=>[{"first_name"=>"test name", "last_name"=>"test", "birth_date"=>"2019-01-22T09:43:39.698Z"}]}}
Permit book params
def book_params
params.require(:book).premit(:first_name, :last_name, authors_attributes: %i[first_name last_name birth_date])
end

Present subset of an object with ActiveModel Serializer

I am using ActiveModel Serializers in a Rails project.
The default serializer for the object is fairly large, and nesting an object in API responses result in rather large JSON objects.
Sometimes, I want to embed an object, but only need a small subset of the object's attributes to be present in the JSON.
Obviously, I could do something like this:
render json: #user, serializer: SmallerUserSerializer
but that would lead to a lot of duplication.
Is there an option that I can pass to the serializer so that it will only include a subset of the serializers attributes? Eg:
class BlogSerializer
# This is pseudocode. Does not actually work.
has_one :user, only_show: [:user_id, :profile_url]
end
Create a method and call to_json on the user object. Then add that method name to your list of attributes. The method can be called user also.
class BlogSerializer
attributes :id, :user
def user
object.user.to_json( only: [ :id, :profile_url ] )
end
end
Use the active model serialzers gem.
Your pseudo code will become the following simple modularized code:
class BlogSerializer < ActiveModel::Serializer
attributes :user_id, :profile_url
end
Guide: http://railscasts.com/episodes/409-active-model-serializers
Create a method and call to_json on the user object. Then add that method name to your list of attributes. The method can be called user also.
class BlogSerializer
require 'json'
attributes :id, :user
def user
JSON.parse "#{object.user.to_json( only: [ :id, :profile_url ] )}"
end
end

Adding fields to a Rails model without saving them in the database in Rails

I am rendering a view based on a JSON object representing a Rails model. This JSON object needs fields that are not part of the Rails model, so I'm adding them in the controller as follows in Example 1:
#Controller
events = Event.where(<query>).each do |event|
event[:duration] = (event.end - event.start)/3600
event[:datatime] = event.start.hour + event.start.min/60.0
...
end
render json: events
This renders my data correctly. However, I get the following deprecation warning:
DEPRECATION WARNING: You're trying to create an attribute `duration'. Writing arbitrary attributes on a model is deprecated. Please just use `attr_writer` etc.
I want to rewrite my code to avoid this warning. If I try treating these extra fields as a standard object attribute, the values are not rendered correctly. Below is my attempt to change to using standard object attributes:
#Controller
events = Event.where(<query>).each do |event|
event.duration = (event.end - event.start)/3600
event.datatime = event.start.hour + event.start.min/60.0
...
end
render json: events
#Model
class Event < ActiveRecord::Base
include ActiveModel::AttributeMethods
attr_accessor :duration, :datatime
This causes the fields to be filled with undefined. This is true if I use attr_writer instead of attr_accessor. How can I fix this problem? Am I forced to store these temporary attributes in the database or am I just using the wrong syntax?
If you want to read and write the accessors use attr_accessor:
class Event < ActiveRecord::Base
attr_accessor :duration, :datatime
end
You do not need to include ActiveModel::AttributeMethods to utilize attr_accessor.
To include the accessors in the JSON tell render to include them:
render json: events, methods: [:duration, :datatime]

Resources