15

There are at least 10 questions on this topic but none of them answer this particular issue. Many of the questions relate to Rails forms like this, which I don't have, or to json structures that are more complicated, like this or this.


EDIT regarding the accepted answer and why this is not an exact duplicate

The linked question in the answer from @CarlosRoque initially looks to be the same problem but it only solves the Rails side of this particular issue.

If you read all the comments you will see multiple attempts at changing the template_params method were made to RENAME or REPLACE the nested attribute "template_items" with "template_items_attributes". This is necessary because Rails accepts_nested_attributes_for requires "_attributes" to be appended to the name, otherwise it cannot see it.

If you examine the monkey patch code in that answer to fix wrap_parameters so that it works for nested attributes, you still have the problem that it won't actually find "template_items" (the nested object) because it does not have the suffix "_attributes".

Therefore to fully solve this the client also had to be modified to send the nested object as "template_items_attributes". For JS clients this can be done by implementing a toJSON() method on the object to modify it during serialization (example here). But be aware that when you deserialize the JSON, you will need to manually create an instance of that object for toJSON() to work (explained why here).


I have a simple has_many / belongs_to:

Models:

class Template < ApplicationRecord belongs_to :account has_many :template_items accepts_nested_attributes_for :template_items, allow_destroy: true end class TemplateItem < ApplicationRecord belongs_to :template validates_presence_of :template enum item_type: {item: 0, heading: 1} end 

The json sent from the client looks like this:

{ "id": "55e27eb7-1151-439d-87b7-2eba07f3e1f7", "account_id": "a61151b8-deed-4efa-8cad-da1b143196c9", "name": "Test", "info": "INFO1234", "title": "TITLE1", "template_items": [ { "is_completed": false, "item_type": "item" }, { "is_completed": false, "item_type": "heading" } ] } 

Sometimes there will be an :id and a :content attribute in each template_item (eg. after they have been created and user starts editing them).

The template_params method of the templates_controller looks like this:

 params.require(:template).permit( :id, :account_id, :name, :title, :info, template_items: [:id, :is_completed, :content, :item_type] ) 

If this was a Rails form then that line would be:

 params.require(:template).permit( :id, :account_id, :name, :title, :info, template_items_attributes: [:id, :is_completed, :content, :item_type] ) 

for saving the nested children objects as part of the parent template update action.

I tried changing the nested param name:

def template_params params.require(:template).permit(:id, :account_id, :name, :title, :info, template_items: [:id, :is_completed, :content, :item_type]) params[:template_items_attributes] = params.delete(:template_items) if params[:template_items] Rails.logger.info params end 

and I can see they are still not permitted:

{ "template" =><ActionController::Parameters { "id" =>"55e27eb7-1151-439d-87b7-2eba07f3e1f7", "account_id" =>"a61151b8-deed-4efa-8cad-da1b143196c9", "name" =>"Test", "info" =>"INFO1234", "title" =>"TITLE1", } permitted:false >, "template_items_attributes" => [ <ActionController::Parameters { "is_completed" =>false, "item_type" =>"item" } permitted:false >, <ActionController::Parameters { "is_completed" =>false, "item_type" =>"item" } permitted:false > ] } 

I also tried merging:

template_params.merge! ({template_items_attributes: params[:template_items]}) if params[:template_items].present? 

Same problem.

So how can I ensure they are permitted and included in template_params WITHOUT just doing .permit! (ie. I don't want to permit everything blindly)?

The controller update method:

def update Rails.logger.info "*******HERE*******" Rails.logger.info template_params @template.template_items = template_params[:template_items_attributes] if @template.update(template_params) render json: @template else render json: ErrorSerializer.serialize(@template.errors), status: :unprocessable_entity end end 

UDPATE

If I send from the client "template_items_attributes" instead of "template_items" inside the parameters to Rails, and then do the recommended template_params like this:

 def template_params params.require(:template).permit(:id, :account_id, :name, :title, :info, template_items_attributes: [:id, :is_completed, :content, :item_type]) end 

it still does not create new children for the template!

With this in place, I output the parameters before and afterwards, like this:

def update Rails.logger.info params Rails.logger.info "*******HERE*******" Rails.logger.info template_params if @template.update(template_params) render json: @template else render json: ErrorSerializer.serialize(@template.errors), status: :unprocessable_entity end end 

And here is the log from this scenario - Rails is STILL completely ignoring the embedded array. Notice that params, just before HERE, shows permitted: false and then afterwards template_params no longer contains the children "template_items_attributes" and is marked permitted:true.

I, [2017-10-20T21:52:39.886104 #28142] INFO -- : Processing by Api::TemplatesController#update as JSON I, [2017-10-20T21:52:39.886254 #28142] INFO -- : Parameters: {"id"=>"55e27eb7-1151-439d-87b7-2eba07f3e1f7", "account_id"=>"a61151b8-deed-4efa-8cad-da1b143196c9", "name"=>"Test", "info"=>"INFO12345", "title"=>"TITLE1", "created_at"=>"2017-10-14T19:30:41.450Z", "updated_at"=>"2017-10-20T17:48:24.909Z", "template_items_attributes"=>[{"is_completed"=>false, "item_type"=>"item"}], "template"=>{"id"=>"55e27eb7-1151-439d-87b7-2eba07f3e1f7", "account_id"=>"a61151b8-deed-4efa-8cad-da1b143196c9", "name"=>"Test", "info"=>"INFO12345", "title"=>"TITLE1", "created_at"=>"2017-10-14T19:30:41.450Z", "updated_at"=>"2017-10-20T17:48:24.909Z"}} D, [2017-10-20T21:52:39.903011 #28142] DEBUG -- : User Load (7.7ms) SELECT "users".* FROM "users" WHERE "users"."uid" = $1 LIMIT $2 [["uid", "[email protected]"], ["LIMIT", 1]] D, [2017-10-20T21:52:40.072148 #28142] DEBUG -- : Template Load (1.4ms) SELECT "templates".* FROM "templates" WHERE "templates"."id" = $1 ORDER BY name ASC LIMIT $2 [["id", "55e27eb7-1151-439d-87b7-2eba07f3e1f7"], ["LIMIT", 1]] I, [2017-10-20T21:52:40.083727 #28142] INFO -- : <ActionController::Parameters {"id"=>"55e27eb7-1151-439d-87b7-2eba07f3e1f7", "account_id"=>"a61151b8-deed-4efa-8cad-da1b143196c9", "name"=>"Test", "info"=>"INFO12345", "title"=>"TITLE1", "created_at"=>"2017-10-14T19:30:41.450Z", "updated_at"=>"2017-10-20T17:48:24.909Z", "template_items_attributes"=>[{"is_completed"=>false, "item_type"=>"item"}], "controller"=>"api/templates", "action"=>"update", "template"=>{"id"=>"55e27eb7-1151-439d-87b7-2eba07f3e1f7", "account_id"=>"a61151b8-deed-4efa-8cad-da1b143196c9", "name"=>"Test", "info"=>"INFO12345", "title"=>"TITLE1", "created_at"=>"2017-10-14T19:30:41.450Z", "updated_at"=>"2017-10-20T17:48:24.909Z"}} permitted: false> I, [2017-10-20T21:52:40.083870 #28142] INFO -- : *******HERE******* D, [2017-10-20T21:52:40.084550 #28142] DEBUG -- : Unpermitted parameters: :created_at, :updated_at I, [2017-10-20T21:52:40.084607 #28142] INFO -- : <ActionController::Parameters {"id"=>"55e27eb7-1151-439d-87b7-2eba07f3e1f7", "account_id"=>"a61151b8-deed-4efa-8cad-da1b143196c9", "name"=>"Test", "title"=>"TITLE1", "info"=>"INFO12345"} permitted: true> D, [2017-10-20T21:52:40.084923 #28142] DEBUG -- : Unpermitted parameters: :created_at, :updated_at D, [2017-10-20T21:52:40.085375 #28142] DEBUG -- : (0.2ms) BEGIN D, [2017-10-20T21:52:40.114015 #28142] DEBUG -- : Account Load (1.2ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT $2 [["id", "a61151b8-deed-4efa-8cad-da1b143196c9"], ["LIMIT", 1]] D, [2017-10-20T21:52:40.131895 #28142] DEBUG -- : Template Exists (0.8ms) SELECT 1 AS one FROM "templates" WHERE "templates"."name" = $1 AND ("templates"."id" != $2) AND "templates"."account_id" = 'a61151b8-deed-4efa-8cad-da1b143196c9' LIMIT $3 [["name", "Test"], ["id", "55e27eb7-1151-439d-87b7-2eba07f3e1f7"], ["LIMIT", 1]] D, [2017-10-20T21:52:40.133754 #28142] DEBUG -- : (0.3ms) COMMIT D, [2017-10-20T21:52:40.137763 #28142] DEBUG -- : CACHE Account Load (0.0ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT $2 [["id", "a61151b8-deed-4efa-8cad-da1b143196c9"], ["LIMIT", 1]] D, [2017-10-20T21:52:40.138714 #28142] DEBUG -- : (0.2ms) BEGIN D, [2017-10-20T21:52:40.141293 #28142] DEBUG -- : User Load (1.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 FOR UPDATE [["id", "88de3be9-6d18-4687-ab80-d50f78638ca9"], ["LIMIT", 1]] D, [2017-10-20T21:52:40.235163 #28142] DEBUG -- : Account Load (0.7ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT $2 [["id", "a61151b8-deed-4efa-8cad-da1b143196c9"], ["LIMIT", 1]] D, [2017-10-20T21:52:40.240997 #28142] DEBUG -- : SQL (1.4ms) UPDATE "users" SET "tokens" = $1, "updated_at" = $2 WHERE "users"."id" = $3 [["tokens", "{\"ryyymFZ7fpH50rMKArjZ2Q\":{\"token\":\"$2a$10$4jkgRe4LBPxJ8fQUOKCSausUi7DbIUD0bE.7ZRoOuTHrRuX6CaWOe\",\"expiry\":1509293414,\"last_token\":\"$2a$10$cpI.mz81JFjQT0J9acCCl.NdrEatI5l17GtrwrAfwyhyN3xRExcaC\",\"updated_at\":\"2017-10-15T17:10:16.996+02:00\"},\"Y2y0maUT5WYSfH6VZeORag\":{\"token\":\"$2a$10$8KERiIwlc3rX.Mdu.CW6wOMLDbVyB2PFCaBIlw7/LUxC3ITpYTISW\",\"expiry\":1509293475,\"last_token\":\"$2a$10$r6Xw6798T1P7UZlTbEaXoeBCl9oK2fMs72ppAtars8Ai/kaE6nE66\",\"updated_at\":\"2017-10-15T17:11:18.066+02:00\"},\"9Cy48CPVj3WhFkEBPUZQ1Q\":{\"token\":\"$2a$10$Qy4JOD8.jIcPhf93MqFCIelnVaA/ssE31w5DlL8MShDuMROsLSNuS\",\"expiry\":1509293942,\"last_token\":\"$2a$10$e6sxklrHRRD1C15Ix/MqQOfACuCMznmzUjF296cpO1ypWVvJ.JFJK\",\"updated_at\":\"2017-10-15T17:19:05.200+02:00\"},\"O5iufW0Gacqs9sIfJ9705w\":{\"token\":\"$2a$10$EkDf7.y3lY9D36lAwNHBGuct97M6/HGDvnrUsD72c8zCsfVd8y9c2\",\"expiry\":1509482450,\"last_token\":\"$2a$10$S0kHEvKxom2Qgdy0r.q0aeTSlSBFkqU4XZeY91n3RkkYkQykmmGVi\",\"updated_at\":\"2017-10-17T21:40:50.300+02:00\"},\"ETOadoEtoxcz6rR6Ced_dA\":{\"token\":\"$2a$10$8t01bWv/PsVojs3cazuSg..FWa9SZwq1/PUDfuN1S4yBxnMFv2zre\",\"expiry\":1509742360,\"last_token\":\"$2a$10$hveuajISXDOjHLm9EkVzvOd3pwKkqE1rQnIFBoojf0vgMLXV2EvVe\",\"updated_at\":\"2017-10-20T21:52:40.233+02:00\"}}"], ["updated_at", "2017-10-20 19:52:40.236607"], ["id", "88de3be9-6d18-4687-ab80-d50f78638ca9"]] D, [2017-10-20T21:52:40.243960 #28142] DEBUG -- : (1.3ms) COMMIT I, [2017-10-20T21:52:40.244504 #28142] INFO -- : Completed 200 OK in 358ms (Views: 1.0ms | ActiveRecord: 37.7ms) 
16
  • Can you also post the controller action you have? Commented Oct 20, 2017 at 17:15
  • Thanks for adding the controller. Its seems the problem could be the fact that you don't have optional:true. Rails 5 forces the parent to be required. belongs_to : template, optional: true . When you try this, I would also recommend using the snippet you have under If this was a Rails form then that line would be when defining Strong Params. See if those edits let you save the model correctly. Commented Oct 20, 2017 at 17:22
  • The parent always exists - this is not the create action. In the create action you can only create a parent template, not children items. But in the update action you can update both the parent and its children. I will try it though as I'm getting pretty crazy, tried a million things in the past 4 hours. Commented Oct 20, 2017 at 17:28
  • Also, you "should not" need the line @template.template_items = template_params[:template_items_attributes] in your actions. Part of what rails does when you define accepts nested attributes for. @template.update(template_params) should also read the nested attributes and update accordingly. Take a look at this blog: devopsdiarist.uk/rails-5-nested-attributes shows a very simple example but with your exact scenario. Commented Oct 20, 2017 at 17:32
  • Good point, I added that before I added accepts_nested_attributes to try and see if I could force creating the items manually. Commented Oct 20, 2017 at 17:33

3 Answers 3

11
+50

I think you forget that params.require(:template).permit( ... is a method that is returning a value and when you call params to modify it later you are only modifying params that have not been permitted yet. what you want to do is swap the order of when you are performing the parameter manipulation.

def template_params params[:template][:template_items_attributes] = params[:template_items_attributes] params.require(:template).permit(:id, :account_id, :name, :title, :info, template_items_attributes: [:id, :is_completed, :content, :item_type]) end 

UPDATE: wrap_parameters was the culprit as it was not including nested parameters in the wrapped params. this fixes the issue

UPDATE: this answer implements a different solution Rails 4 Not Updating Nested Attributes Via JSON

This is a long open request in github!! crazy https://github.com/rails/rails/pull/19254

UPDATE: this was finally merged in AR 6 https://github.com/rails/rails/commit/62b7ad46c0f3ff24980956daadba46ccb2568445

Sign up to request clarification or add additional context in comments.

9 Comments

Thanks, I tried that, they are still marked as permitted: false.
Looking at this again, this SHOULD be the correct answer. Why the hell is Rails marking the params as false after doing this?
are all parameters marked as false? including the template parameters? then it is probably because everything needs to go inside template:
I have made a change to my answer to take that into account.
So I just tested that idea. I put the template_params method back to how it was at the very start. Sent a request from client with the child already called "template_items_attributes" in the params, and in the update action it is no longer there! Rails is removing the entire array...maybe because there are no ids?
|
1

Problem Your problem is in your update action you are trying to save the associations on @template that have not been built yet. Because there are no 'ids' coming in with the hash, the update function just ignores them.

The Solution Is to iterate on the array of association hashes coming in through to the update action, and build them on the @template before calling update on the @template.

Here's the sudo code (haven't tried it, so don't copy paste) :

models

class Template < ApplicationRecord belongs_to :account has_many :template_items accepts_nested_attributes_for :template_items, allow_destroy: true end class TemplateItem < ApplicationRecord belongs_to :template, optional:true # <------ CHANGE validates_presence_of :template enum item_type: {item: 0, heading: 1} end 

Strong params definition

params.require(:template).permit( :id, :account_id, :name, :title, :info, template_items_attributes: [:id, :is_completed, :content, :item_type] ) 

Update Action

def update template_params.template_items.each do |item_hash| # <------ CHANGE @template.template_items.build(item_hash) end if @template.update(template_params) render json: @template else render json: ErrorSerializer.serialize(@template.errors), status: :unprocessable_entity end end 

10 Comments

Thanks. I will try your code but it won't work because the fundamental problem is that accepts_nested_attributes_for adds an "attribute_writer" which requires that the params for children ends with the word "_attributes". My request doesn't have that in the json. No matter how I try to add that to "tempalte_items" in the template_params method, Rails keeps throwing the child attributes away or always marks them false! :(
FYI the optional: true is only ensuring that 'Template must exist' before creating the children, so surely that's not the issue, since they will always exist in my scenario, which is updating an existing Template but at the same time creating new children.
Right, in your case you should not need optional: true because you always create the template first.
I totally understand what you are saying with this answer. So I tried your code now (with the changes). template_params.template_items.each -> NoMethodError (undefined method `template_items' for #<ActionController::Parameters:0x007fc082fef1b0>). I also tried renaming that parameter array like before to template_items_attributes, but it is always marked false or just removed by the permit code. So how can the update ever work? It never gets the children.
eg. in the update, when I try this: template_params[:template_items_attributes].each it throws Forbidden Attributes error.
|
-1
 def template_params template_params = params.require(:template).permit(:id, :account_id, :name,:title, :info, template_items: [:id, :is_completed, :content, :item_type]) template_params[:template_items_attributes] = template_params.delete :template_items template_params.permit! end 

1 Comment

Posting code without an explanation is not very helpful. Also, this does not work because they need to be 'template_items_attributes' to even reach this part of the code -> so you cannot see 'template_items' inside the method, Rails has already removed them.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.