title: Rails 5.1 API - how to permit params for nested JSON object's attributes
body: <p>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 <a href="https://stackoverflow.com/questions/32529757/how-can-i-get-strong-parameters-to-permit-nested-fields-for-attributes">this</a>, which I don't have, or to json structures that are more complicated, like <a href="https://stackoverflow.com/questions/18436741/rails-4-strong-parameters-nested-objects?noredirect=1&amp;lq=1">this</a> or <a href="https://stackoverflow.com/questions/29707624/how-to-use-rails-action-controller-nested-params-to-permit-a-specific-attributes">this</a>.</p>
  
  <hr>
  
  <p><strong>EDIT regarding the accepted answer and why this is not an exact duplicate</strong></p>
  
  <p>The linked question in the answer from @CarlosRoque initially looks to be the same problem but <em>it only solves the Rails side of this particular issue.</em></p>
  
  <p>If you read all the comments you will see multiple attempts at changing the <code>template_params</code> method were made to RENAME or REPLACE the nested attribute "template_items" with "template_items_attributes". This is necessary because Rails <code>accepts_nested_attributes_for</code> requires "_attributes" to be appended to the name, otherwise it cannot see it.</p>
  
  <p>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".</p>
  
  <p>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 <a href="https://stackoverflow.com/questions/46858650/angular2-4-what-is-the-correct-way-to-add-a-tojson-function-to-a-typescript-cl/46862365#46862365">here</a>). 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 <a href="https://stackoverflow.com/questions/46863066/angular4-why-does-custom-tojson-only-get-called-on-new-objects">here</a>).</p>
  
  <hr>
  
  <p>I have a simple has_many / belongs_to:</p>
  
  <p>Models:</p>
  
  <pre><code>class Template &lt; ApplicationRecord
    belongs_to :account
    has_many :template_items
    accepts_nested_attributes_for :template_items, allow_destroy: true
  end
  
  
  class TemplateItem &lt; ApplicationRecord  
    belongs_to :template
    validates_presence_of :template
  
    enum item_type: {item: 0, heading: 1} 
  end
  </code></pre>
  
  <p>The json sent from the client looks like this:</p>
  
  <pre><code>{
    "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"
      }
    ]
  }
  </code></pre>
  
  <p>Sometimes there will be an <code>:id</code> and a <code>:content</code> attribute in each template_item (eg. after they have been created and user starts editing them).</p>
  
  <p>The <code>template_params</code> method of the <code>templates_controller</code> looks like this:</p>
  
  <pre><code>   params.require(:template).permit(
        :id, :account_id, :name, :title, :info, 
        template_items: [:id, :is_completed, :content, :item_type]
    )
  </code></pre>
  
  <p>If this was a Rails form then that line would be:</p>
  
  <pre><code>   params.require(:template).permit(
        :id, :account_id, :name, :title, :info, 
        template_items_attributes: [:id, :is_completed, :content, :item_type]
    )
  </code></pre>
  
  <p>for saving the nested children objects as part of the parent template update action.</p>
  
  <p>I tried changing the nested param name:</p>
  
  <pre><code>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
  </code></pre>
  
  <p>and I can see they are still not permitted:</p>
  
  <pre><code>{  
        "template"   =&gt;&lt;ActionController::Parameters   {  
        "id"      =&gt;"55e27eb7-1151-439d-87b7-2eba07f3e1f7",
        "account_id"      =&gt;"a61151b8-deed-4efa-8cad-da1b143196c9",
        "name"      =&gt;"Test",
        "info"      =&gt;"INFO1234",
        "title"      =&gt;"TITLE1",
     }   permitted:false   &gt;,
     "template_items_attributes"   =&gt;   [  
        &lt;ActionController::Parameters      {  
           "is_completed"         =&gt;false,
           "item_type"         =&gt;"item"
        }      permitted:false      &gt;,
        &lt;ActionController::Parameters      {  
           "is_completed"         =&gt;false,
           "item_type"         =&gt;"item"
        }      permitted:false      &gt;
     ]
  }
  </code></pre>
  
  <p>I also tried merging:</p>
  
  <pre><code>template_params.merge! ({template_items_attributes:
  params[:template_items]}) if params[:template_items].present?
  </code></pre>
  
  <p>Same problem.</p>
  
  <p>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)?</p>
  
  <p>The controller update method:</p>
  
  <pre><code>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
  </code></pre>
  
  <p><strong>UDPATE</strong></p>
  
  <p>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:</p>
  
  <pre><code>    def template_params
        params.require(:template).permit(:id, :account_id, :name, :title, :info, template_items_attributes: [:id, :is_completed, :content, :item_type])
      end
  </code></pre>
  
  <p>it still does not create new children for the template!</p>
  
  <p>With this in place, I output the parameters before and afterwards, like this:</p>
  
  <pre><code>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
  </code></pre>
  
  <p>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.</p>
  
  <pre><code>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"=&gt;"55e27eb7-1151-439d-87b7-2eba07f3e1f7", "account_id"=&gt;"a61151b8-deed-4efa-8cad-da1b143196c9", "name"=&gt;"Test", "info"=&gt;"INFO12345", "title"=&gt;"TITLE1", "created_at"=&gt;"2017-10-14T19:30:41.450Z", "updated_at"=&gt;"2017-10-20T17:48:24.909Z", "template_items_attributes"=&gt;[{"is_completed"=&gt;false, "item_type"=&gt;"item"}], "template"=&gt;{"id"=&gt;"55e27eb7-1151-439d-87b7-2eba07f3e1f7", "account_id"=&gt;"a61151b8-deed-4efa-8cad-da1b143196c9", "name"=&gt;"Test", "info"=&gt;"INFO12345", "title"=&gt;"TITLE1", "created_at"=&gt;"2017-10-14T19:30:41.450Z", "updated_at"=&gt;"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 -- : &lt;ActionController::Parameters {"id"=&gt;"55e27eb7-1151-439d-87b7-2eba07f3e1f7", "account_id"=&gt;"a61151b8-deed-4efa-8cad-da1b143196c9", "name"=&gt;"Test", "info"=&gt;"INFO12345", "title"=&gt;"TITLE1", "created_at"=&gt;"2017-10-14T19:30:41.450Z", "updated_at"=&gt;"2017-10-20T17:48:24.909Z", "template_items_attributes"=&gt;[{"is_completed"=&gt;false, "item_type"=&gt;"item"}], "controller"=&gt;"api/templates", "action"=&gt;"update", "template"=&gt;{"id"=&gt;"55e27eb7-1151-439d-87b7-2eba07f3e1f7", "account_id"=&gt;"a61151b8-deed-4efa-8cad-da1b143196c9", "name"=&gt;"Test", "info"=&gt;"INFO12345", "title"=&gt;"TITLE1", "created_at"=&gt;"2017-10-14T19:30:41.450Z", "updated_at"=&gt;"2017-10-20T17:48:24.909Z"}} permitted: false&gt;
  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 -- : &lt;ActionController::Parameters {"id"=&gt;"55e27eb7-1151-439d-87b7-2eba07f3e1f7", "account_id"=&gt;"a61151b8-deed-4efa-8cad-da1b143196c9", "name"=&gt;"Test", "title"=&gt;"TITLE1", "info"=&gt;"INFO12345"} permitted: true&gt;
  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)
  </code></pre>