I am having difficulty sending a JSON payload to an endpoint that contains a nested resource that I would like created with associations in-tact and persisted... no matter what I do I am presented in the console with Unpermitted parameter: :ingredients.
models/meal.rb
class Meal < ApplicationRecord
has_many :ingredients
accepts_nested_attributes_for :ingredients
end
models/ingredient.rb
class Ingredient < ApplicationRecord
belongs_to :meal
end
controller/meals_controller.rb
class MealsController < ApplicationController
def create
#meal = Meal.new(meal_params)
puts meal_params.inspect
# just want to see the parameters in the log
# ...
end
private
def meal_params
params.require(:meal).permit(
:name,
ingredients_attributes: [:id, :text]
)
end
end
db/migrate/xxx_create_inredients.rb
class CreateIngredients < ActiveRecord::Migration[5.1]
def change
create_table :ingredients do |t|
t.belongs_to :meal, index: true
t.string :text
t.timestamps
end
end
end
request JSON payload
{
"meal": {
"name": "My Favorite Meal",
"ingredients": [
{ "text": "First ingredient" },
{ "text": "Second ingredient" }
]
}
}
I tried another approach from a SO article encountering a similar problem that seemed to recognize the ingredients parameter, but ultimately threw a 500:
params.require(:meal).permit(:name, { ingredients: [:id, :text] })
When doing that I receive the following:
ActiveRecord::AssociationTypeMismatch (Ingredient(#70235637684840) expected, got {"text"=>"First Ingredient"} which is an instance of ActiveSupport::HashWithIndifferentAccess(#70235636442880))
Any help in pointing out my flaw is much appreciated. And yes, I want the nested ingredients resource to be part of the payload going to this endpoint.
The only problem that you have here is that you have in your meal params ingredients_attributes
def meal_params
params.require(:meal).permit(
:name,
ingredients_attributes: [:id, :text]
)
end
so in your payload you need to have it with that key too
{
"meal": {
"name": "My Favorite Meal",
"ingredients_attributes": [
{ "text": "First ingredient" },
{ "text": "Second ingredient" }
]
}
}
they need to match.
Related
In my Rails (api only) learning project, I have 2 models, Group and Artist, that have a many-to-many relationship with a joining model, Role, that has additional information about the relationship. I have been able to save m2m relationships before by saving the joining model by itself, but here I am trying to save the relationship as a nested relationship. I'm using the jsonapi-serializer gem, but not married to it nor am I tied to the JSON api spec. Getting this to work is more important than following best practice.
With this setup, I'm getting a 500 error when trying to save with the following errors:
Unpermitted parameters: :artists, :albums and ActiveModel::UnknownAttributeError (unknown attribute 'relationships' for Group.)
I'm suspecting that my problem lies in the strong param and/or the json payload.
Models
class Group < ApplicationRecord
has_many :roles
has_many :artists, through: :roles
accepts_nested_attributes_for :artists, :roles
end
class Artist < ApplicationRecord
has_many :groups, through: :roles
end
class Role < ApplicationRecord
belongs_to :artist
belongs_to :group
end
Controller#create
def create
group = Group.new(group_params)
if group.save
render json: GroupSerializer.new(group).serializable_hash
else
render json: { error: group.errors.messages }, status: 422
end
end
Controller#group_params
def group_params
params.require(:data)
.permit(attributes: [:name, :notes],
relationships: [:artists])
end
Serializers
class GroupSerializer
include JSONAPI::Serializer
attributes :name, :notes
has_many :artists
has_many :roles
end
class ArtistSerializer
include JSONAPI::Serializer
attributes :first_name, :last_name, :notes
end
class RoleSerializer
include JSONAPI::Serializer
attributes :artist_id, :group_id, :instruments
end
Example JSON payload
{
"data": {
"attributes": {
"name": "Pink Floyd",
"notes": "",
},
"relationships": {
"artists": [{ type: "artist", "id": 3445 }, { type: "artist", "id": 3447 }]
}
}
Additional Info
It might help to know that I was able to save another model with the following combination of json and strong params.
# Example JSON
"data": {
"attributes": {
"title": "Wish You Were Here",
"release_date": "1975-09-15",
"release_date_accuracy": 1
"notes": "",
"group_id": 3455
}
}
# in albums_controller.rb
def album_params
params.require(:data).require(:attributes)
.permit(:title, :group_id, :release_date, :release_date_accuracy, :notes)
end
From looking at https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html I think the data format that Rails is normally going to expect will look something like:
{
"group": {
"name": "Pink Floyd",
"notes": "",
"roles_attributes": [
{ "artist_id": 3445 },
{ "artist_id": 3447 }
]
}
}
with a permit statement that looks something like (note the . before permit has moved):
params.require(:group).
permit(:name, :notes, roles_attributes: [:artist_id])
I think you have a few options here:
Change the data format coming into the action.
Craft a permit statement that works with your current data (not sure how tricky that is), you can test your current version in the console with:
params = ActionController::Parameters.new({
"data": {
"attributes": {
"name": "Pink Floyd",
"notes": "",
},
"relationships": {
"artists": [{ type: "artist", "id": 3445 }, { type: "artist", "id": 3447 }]
}
}
})
group_params = params.require(:data).
permit(attributes: [:name, :notes],
relationships: [:artists])
group_params.to_h.inspect
and then restructure the data to a form the model will accept; or
Restructure the data before you try to permit it e.g. something like:
def group_params
params_hash = params.to_unsafe_h
new_params_hash = {
"group": params_hash["data"]["attributes"].merge({
"roles_attributes": params_hash["data"]["relationships"]["artists"].
map { |a| { "artist_id": a["id"] } }
})
}
new_params = ActionController::Parameters.new(new_params_hash)
new_params.require(:group).
permit(:name, :notes, roles_attributes: [:artist_id])
end
But ... I'm sort of hopeful that I'm totally wrong and someone else will come along with a better solution to this stuff.
I'm using Netflix's jsonapi-rails gem to serialize my API. I need to build a response.json object that includes the associated comments for a post.
Post model:
class Post < ApplicationRecord
has_many :comments, as: :commentable
end
Polymorphic Comment model
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
PostSerializer
class PostSerializer
include FastJsonapi::ObjectSerializer
attributes :body
has_many :comments, serializer: CommentSerializer, polymorphic: true
end
CommentSerializer
class CommentSerializer
include FastJsonapi::ObjectSerializer
attributes :body, :id, :created_at
belongs_to :post
end
Posts#index
class PostsController < ApplicationController
def index
#posts = Post.all
hash = PostSerializer.new(#posts).serialized_json
render json: hash
end
end
What I've got so far only gives me the comment type and id, but I NEED the comment's body as well.
Please help!
Thanks in advance~!
Although not very intuitive this behaviour is there by design. According to the JSON API relationship data and actual related resource data belong in different objects of the structure
You can read more here:
Fetching Relationships
Inclusion of Related Resources
To include the body of the Comments your serializers would have to be:
class PostSerializer
include FastJsonapi::ObjectSerializer
attributes :body, :created_at
has_many :comments
end
class CommentSerializer
include FastJsonapi::ObjectSerializer
attributes :body, :created_at
end
and your controller code:
class HomeController < ApplicationController
def index
#posts = Post.all
options = {include: [:comments]}
hash = PostSerializer.new(#posts, options).serialized_json
render json: hash
end
end
The response for a single Post would look something like this:
{
"data": [
{
"attributes": {
"body": "A test post!"
},
"id": "1",
"relationships": {
"comments": {
"data": [
{
"id": "1",
"type": "comment"
},
{
"id": "2",
"type": "comment"
}
]
}
},
"type": "post"
}
],
"included": [
{
"attributes": {
"body": "This is a comment 1 body!",
"created_at": "2018-05-06 22:41:53 UTC"
},
"id": "1",
"type": "comment"
},
{
"attributes": {
"body": "This is a comment 2 body!",
"created_at": "2018-05-06 22:41:59 UTC"
},
"id": "2",
"type": "comment"
}
]
}
I haven't found the exactly same question on Stackoverflow. Besides AMS changes so rapidly, that even 2-year-old answers get outdated often.
I use Rails 5 API-only and the gem 'active_model_serializers' (AMS) (ver. 0.10.6).
I also use the JSONAPI response format - not simply JSON.
I need to render a nested include - not just nested relations as of now.
Code example:
serializer1:
class QuestionSerializer < ActiveModel::Serializer
attributes :id, :title, :created_at, :updated_at
belongs_to :user
end
serializer2:
class UserSerializer < ActiveModel::Serializer
attributes :id, :email
has_many :cities
end
serializer3:
class CitySerializer < ActiveModel::Serializer
attributes :id, :name
end
controller:
def index
#questions = Question.all
render json: #questions, include: [:user, 'user.city']
end
I get this response:
{
"data": [
{
"id": "1",
"type": "questions",
"attributes": {
"title": "First",
"created-at": "2017-03-27T13:22:15.548Z",
"updated-at": "2017-03-27T13:22:16.463Z"
},
"relationships": {
"user": {
"data": {
"id": "3",
"type": "users"
}
}
}
}
],
"included": [
{
"id": "3",
"type": "users",
"attributes": {
"email": "client2#example.com"
},
"relationships": {
"cities": {
"data": [
{
"id": "75",
"type": "cities"
}
]
}
}
}
]
}
I really do get a nested relation city. I even get the city id.
But the problem is - how do I get other city attributes like name? I need another include section - maybe inside current include section (nested?).
How to do that? (without any additional gems)
I found some solution. I don't know about how clean is it. It is based on 2 prerequisites:
https://github.com/rails-api/active_model_serializers/blob/db6083af2fb9932f9e8591e1d964f1787aacdb37/docs/general/adapters.md#included
ActiveModel Serializers: has_many with condition at run-time?
I applied the conditional relations and user-all-permissive include:
controller:
#questions = Question.all
render json: #questions,
show_user: (param? params[:user]),
show_cities: (param? params[:city]),
include: [:user, "user.**"]
serializer1:
class QuestionSerializer < ActiveModel::Serializer
attributes :id, :title, :created_at, :updated_at
belongs_to :user, if: -> { should_render_user }
def should_render_user
#instance_options[:show_user]
end
end
serializer2:
class UserSerializer < ActiveModel::Serializer
attributes :id, :email
has_many :cities, if: -> { should_render_cities }
def should_render_cities
#instance_options[:show_cities]
end
end
serializer3:
class CitySerializer < ActiveModel::Serializer
attributes :id, :name
end
helper:
def param? param
param && param != "false" && param != "nil"
end
Conditional relations allow to control which include's actually to render.
The following should do it
render json: #questions, include: [:categories, user: :city]
You should also include the user in the parameter 'fields' and using strings instead of symbols for the parameter include
render json: #questions, fields: [:title, :user], include: ['user', 'user.cities']
I have a model event and another model event_rule
class Event < ApplicationRecord
has_many :event_rules
end
class EventRule < ApplicationRecord
belongs_to :event
end
I have written an api event#create for saving an event. Here's the body of the POST request:
{
"name": "asd",
"code": "ad",
"isActive": true,
"description": "asd",
"notes": "",
"goalAmount": 0,
"exportId": "",
"defaultCurrency": 1,
"eventStartDate": "2017-04-25T18:30:00.000Z",
"eventEndDate": "2017-04-27T18:30:00.000Z",
"eventRules": [
{
"extraInformation": "{}",
"lookupKeyValuePairId": 40
}
]
}
Here's params hash:
Parameters: {"name"=>"asd", "code"=>"ad", "is_active"=>true, "description"=>"asd", "notes"=>"", "goal_amount"=>0, "export_id"=>"", "default_currency"=>1, "event_start_date"=>"2017-04-25T18:30:00.000Z", "event_end_date"=>"2017-04-27T18:30:00.000Z", "event_rules"=>[{"extra_information"=>"{}", "lookup_key_value_pair_id"=>40}], "client_id"=>"0", "event"=>{"name"=>"asd", "code"=>"ad", "description"=>"asd", "is_active"=>true, "goal_amount"=>0, "export_id"=>"", "event_start_date"=>"2017-04-25T18:30:00.000Z", "event_end_date"=>"2017-04-27T18:30:00.000Z", "default_currency"=>1, "notes"=>""}}
I want the 'event_rules' to be included INSIDE the event. How can do this?
def create
# initialize Event object with `event_params`
event = Event.new(event_params)
# initialize EventRule object per each `event_rule_params`, and associate the EventRule as part of `event.event_rules`
event_rules_params.each do |event_rule_params|
event.event_rules << EventRule.new(event_rule_params)
end
if event.save
# SUCCESS
else
# FAILURE
end
end
private
def event_params
params.require(:event).permit(:name, :code, :is_active, :description, :notes, :goal_amount, :export_id, :default_currency, :event_start_date, :event_end_date, :notes)
end
def event_rules_params
params.require(:event).fetch(:event_rules, []).permit(:extra_information, :lookup_key_value_pair_id)
end
Alternative Rails-way Solution:
if you have control over the parameters that get sent, reformat your request into something like the following (take note of changing event_rules into event_rules_attributes -- Rails Standard) (More Info Here)
Parameters: {
"event"=>{
"name"=>"asd",
"code"=>"ad",
"description"=>"asd",
"is_active"=>true,
"goal_amount"=>0,
"export_id"=>"",
"event_start_date"=>"2017-04-25T18:30:00.000Z",
"event_end_date"=>"2017-04-27T18:30:00.000Z",
"default_currency"=>1,
"notes"=>"",
"event_rules_attributes"=>[
{
"extra_information"=>"{}",
"lookup_key_value_pair_id"=>40
}
]
}
}
# controllers/events_controller.rb
def create
event = Event.new(event_params)
if event.save
# SUCCESS
else
# FAILURE
end
end
private
def event_params
params.require(:event).permit(:name, :code, :is_active, :description, :notes, :goal_amount, :export_id, :default_currency, :event_start_date, :event_end_date, :notes, event_rules_attributes: [:extra_information, :lookup_key_value_pair_id])
end
# models/event.rb
accepts_nested_attributes_for :event_rules
I am using active_model_serializers gem for the first time. Version which I'm using is 0.10.2
I have three models with associations like this:
class Song < ActiveRecord::Base
has_many :questions
end
class Question< ActiveRecord::Base
belongs_to :song
has_many :answers
end
class Answer< ActiveRecord::Base
belongs_to :question
end
I've generated three serializers like this:
class SongSerializer < ActiveModel::Serializer
attributes :id, :audio, :image
has_many :questions
end
class QuestionSerializer < ActiveModel::Serializer
attributes :id, :text
belongs_to :song
has_many :answers
end
class AnswerSerializer < ActiveModel::Serializer
attributes :id, :text
belongs_to :question
end
but unfortunately my json response doesn't show me the question's answers, but songs and questions are showing.
after some googling I tried to add
ActiveModelSerializers.config.default_includes = '**'
or from documentation like this:
class Api::SongsController < ApplicationController
def index
songs = Song.all
render json: songs, include: '**' #or with '*'
end
end
but this led me to stack level too deep error
So what should I do in order to get json response to looks like this -
{
"id": "1",
"audio": "...",
"image": "...",
"questions": [
{
"id": "1",
"text": ".....",
"answers": [
{
"id": "1",
"text": "...."
},
{
"id": "2",
"text": "..."
}
]
},
{
"id": "2",
"text": "....."
}
]
}
because simply adding associations like I would do in models are not helping for the third association.
Any help would be appreciated!
So finally after some more searching I found solution which worked. I had to add to my controller include with nested models.
class Api::SongsController < ApplicationController
def index
songs = Song.all
render json: songs, include: ['questions', 'questions.answers']
end
end
And it worked like a charm!
You can do it in your controller with the following structure
respond_with Song.all.as_json(
only: [ :id, :audio, :image ],
include: [
{
questions: {
only: [:id, :text],
include: {
anwers: {
only: [ :id, :text ]
}
}
}
}
]
)