Rails 5+ API: single endpoint for both individual JSON object and array of JSON objects Rails 5+ API: single endpoint for both individual JSON object and array of JSON objects json json

Rails 5+ API: single endpoint for both individual JSON object and array of JSON objects


In the case you send a JSON array (like [{ child_name: "Alpha" }, { child_name: "Bravo" }]) to an endpoint, Rails stores it into _json of the params hash. The reason for doing this is, that unlike a single JSON object, an array cannot be extracted into a hash, without introducing another key.

Let's say, that we post { child_name: "Alpha" } to an endpoint. The params will look like:

{"child_name"=>"Alpha", "format"=>:json, "controller"=>"api/children", "action"=>"create"}

Now if we post the array [{ child_name: "Alpha" }, { child_name: "Bravo" }] and it would be blindly extracted into the hash like a JSON object the result would look like:

{[{ child_name: "Alpha" }, { child_name: "Bravo" }], "format"=>:json, "controller"=>"api/children", "action"=>"create"}

This is not a valid hash! So Rails wraps your JSON array into _json and it becomes:

{"_json" => [{ child_name: "Alpha" }, { child_name: "Bravo" }], "format"=>:json, "controller"=>"api/children", "action"=>"create"}

This happens here in the actionpack. Yes, it seems a bit hacky, but the only and maybe cleaner solution would be to wrap all data from the request body into one key like data.

So to your actual question: How can I manage a JSON object and JSON array at a single endpoint?

You can use strong params like ..

def children_params  model_attributes = [:child_name]  if params.key? '_json'    params.permit(_json: model_attributes)['_json']   else    params.permit(*model_attributes)  endend

.. and in the create action would look like ..

def create  result = Children.create!(children_params)  if children_params.kind_of? Array    @children = result    render :index, status: :created  else    @child = result    render :show, status: :created  endend

Of course you would need to adapt it a bit for your specific use case. From the design perspective of your API, I think it's okay doing it like that. Maybe this should be asked within a separate question.