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", "rmcsharry+owner@gmail.com"], ["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)
rmcsharry
  • 5,363
  • 6
  • 65
  • 108
  • Can you also post the controller action you have? – Shaunak Oct 20 '17 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. – Shaunak Oct 20 '17 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. – rmcsharry Oct 20 '17 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: http://devopsdiarist.uk/rails-5-nested-attributes/ shows a very simple example but with your exact scenario. – Shaunak Oct 20 '17 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. – rmcsharry Oct 20 '17 at 17:33
  • Ok by adding optional: true and using the forms code, I now get this error: ArgumentError (When assigning attributes, you must pass a hash as an argument.). Also when I log the params the entire hash is now marked permitted: false - that explains the error! – rmcsharry Oct 20 '17 at 17:36
  • Ok, the update action now fires, log says Template Exists and the nested params are totally ignored. Only the parent is updated, no children are created. – rmcsharry Oct 20 '17 at 17:48
  • okay now you are on the right track. This now happens because in create you never create associations. So there is nothing to update. update only works when there is an ID coming in for association. if you are going to create an association from update, you need to first build them in edit action. Or iterate on your new associations in update and before you update template , in a each loop use `@template.build_template_item(Item)` – Shaunak Oct 20 '17 at 17:53
  • I think I can form an answer now. Give me a few. I don't have active dev env right now, so had to get info from comments :D – Shaunak Oct 20 '17 at 17:56
  • Thanks, so I have a catch-22 as there is no edit action. It's an api. The template already exists and in this update I need to add new children to it. Hence why there are no ids in the json. Hmmm...so I guess I will have to build the children in the template update action (maybe by calling a build_children method on the template model). – rmcsharry Oct 20 '17 at 17:56
  • Yep you need to build associations in update by looping on them, check my updated comment. I am posting an answer shortly. – Shaunak Oct 20 '17 at 17:59
  • @rmcsharry, have you tried wrapping the nested association's attributes in the controller? – kmanzana Oct 20 '17 at 22:41
  • Dup of https://stackoverflow.com/questions/19574595/rails-4-not-updating-nested-attributes-via-json/27609764? – kmanzana Oct 20 '17 at 22:44
  • @kmanzana Indeed this appears to be a duplicate and I have implemented the monkey_patch provided by lakesare in his answer to that question. – rmcsharry Oct 20 '17 at 22:51
  • Possible duplicate of [Rails 4 Not Updating Nested Attributes Via JSON](https://stackoverflow.com/questions/19574595/rails-4-not-updating-nested-attributes-via-json) – kmanzana Oct 21 '17 at 00:40
  • @kmanzana It turns out the Rails nested attributes is only one half of the problem. – rmcsharry Oct 21 '17 at 10:49

3 Answers3

11

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

Carlos Roque
  • 448
  • 5
  • 11
  • Thanks, I tried that, they are still marked as permitted: false. – rmcsharry Oct 20 '17 at 17:04
  • Looking at this again, this SHOULD be the correct answer. Why the hell is Rails marking the params as false after doing this? – rmcsharry Oct 20 '17 at 18:20
  • are all parameters marked as false? including the template parameters? then it is probably because everything needs to go inside template: – Carlos Roque Oct 20 '17 at 18:26
  • I have made a change to my answer to take that into account. – Carlos Roque Oct 20 '17 at 18:30
  • Thanks. The template_items_attributes are removed when I examine template_params in the update action. I am rapidly concluding this is not possible in Rails and that the request must ensure the parameter containing the children ends with "_attributes" before hitting the controller. – rmcsharry Oct 20 '17 at 18:43
  • 1
    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? – rmcsharry Oct 20 '17 at 18:48
  • Can you share a params dump before templates_params and the result of templates_params? – Carlos Roque Oct 20 '17 at 19:27
  • Oh... I just realized that you are using delete. This returns a key pair object I will update my example to correct this – Carlos Roque Oct 20 '17 at 19:30
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/157165/discussion-between-carlos-roque-and-rmcsharry). – Carlos Roque Oct 20 '17 at 19:58
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
Shaunak
  • 17,377
  • 5
  • 53
  • 84
  • 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! :( – rmcsharry Oct 20 '17 at 18:12
  • 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. – rmcsharry Oct 20 '17 at 18:13
  • Right, in your case you should not need `optional: true` because you always create the template first. – Shaunak Oct 20 '17 at 18:15
  • 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 #). 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. – rmcsharry Oct 20 '17 at 18:18
  • eg. in the update, when I try this: template_params[:template_items_attributes].each it throws Forbidden Attributes error. – rmcsharry Oct 20 '17 at 18:26
  • Here is the really weird thing: if I just do params.permit! then your update code works and builds the children fine. Something is very weird about strong params in this situation of adding new children to an existing parent. Yet the article you linked to is EXACTLY this scenario and he never mentions having to build the children in the update action. – rmcsharry Oct 20 '17 at 18:51
  • Right. That strong param definition should work. And yet I have experienced wonkiness before with naming conventions when you have a model with two words. I would check all the singular/plural conventions when referring to template_items model. There could be a bug there is why they are not getting white listed properly. – Shaunak Oct 20 '17 at 19:03
  • You just read my mind - I am in the process of renaming the model to 'items' and see what happens. – rmcsharry Oct 20 '17 at 19:04
  • Ok, same result. Conclusion: this is only supported when creating a parent - then it will auto-create new children. If you are updating a parent, you cannot use this technique to simultaneously create new children. The article you linked is the former and I am trying to do the latter. – rmcsharry Oct 20 '17 at 19:13
  • Although that article says "accepts_nested_attributes_for is an ActiveRecord directive that allows one model to create or update instances of other related models" - could be wrong I suppose. – rmcsharry Oct 20 '17 at 19:16
-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
Sathibabu P
  • 649
  • 1
  • 7
  • 15
  • 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. – rmcsharry Oct 26 '20 at 19:42