I work in a classic Rails project which contains its own API for mobile devices.
When I am sending a JSON, with all attributes a model owns to my API, for example a user object with its nested user profile attributes, it works fine.
The JSON looks like this:
{
"email": "user-1#example.com",
"user_profile_attributes": {
"display_name": "Awesome user",
"field_a": "string",
"field_b": "string"
}
}
When I now remove a field like field_a from my JSON request, I would expect that Rails backend will ignore the field when updating a database record, using the update method of ActiveRecord. Unfortunately ActiveRecord decides to nullify my missing field instead.
The JSON I am sending without field_a looks like that:
{
"email": "user-1#example.com",
"user_profile_attributes": {
"display_name": "Awesome user",
"field_b": "string"
}
}
The INSERT in my logfile shows me that the field is set to NULL and in my database the field is also set to NULL after update is called on my params.
What I would like to know is if that is the correct behaviour and if this behaves the same in a regular Rails API only projects. How could I prevent Rails or ActiveRecord to not write NULL to fields in the database which are not present in my posted JSON request to the API? Also how can I prevent Rails or ActiveRecord from deleting a nested relation object when its not part of my request JSON? For example if you delete the entire user_profile_attributes node, ActiveRecord will delete it.
My update method in my controller looks like this:
def update
respond_to do |format|
if #current_user.update(update_user_params)
format.json { render 'api/app/v1/users/show', status: :ok, locals: { user: #current_user } }
else
render json: #current_user.errors, status: :unprocessable_entity
end
end
end
def update_user_params
params.require(:user).permit(
:email,
:password,
user_profile_attributes: [:display_name, :field_a, :field_b]
)
end
For a better code demonstration I've written a sample project with Rails 6 which does the same what I do in my project. It includes also an openapi.yml for Pawn, Insomnia or Postman to test the projects API at /api/app/v1 easily.
The code for the update method I use is at that position on my users_controller.rb:
https://github.com/fuxx/update-db-question/blob/master/app/controllers/api/app/v1/users_controller.rb#L16
My sample project is located here on GitHub:
https://github.com/fuxx/update-db-question
Thanks in advance!
Try changing accepts_nested_attributes_for :user_profile to accepts_nested_attributes_for :user_profile, update_only: true in your User model.
Related
I generated a new rails 5 --api --database=postgresql app the other day and only created one scaffold (Hero). I'm wondering how the strong parameters work in rails as I am seeing some odd behavior:
Controller looks like:
def create
hero = Hero.new(hero_params)
if hero.save
render json: hero, status: :created, location: hero
else
render json: hero.errors, status: :unprocessable_entity
end
end
My hero_params look like this:
def hero_params
params.require(:hero).permit(:name)
end
So I would assume that the client is required to submit a hash containing a "hero" key and it's allowed to have a "name" subkey that is allowed to be mass assigned when this controller action is called.
Meaning, the JSON should look like this:
{
"hero": {
"name": "test"
}
}
All is well but here is where I am seeing strange behavior. When the user submits the exact JSON as above, the parameters come in as:
Parameters: {"hero"=>{"name"=>"test"}}
Now if the user submits just:
{ "name": "test" }
It still creates a new resource and the parameters come in as:
Parameters: {"name"=>"test", "hero"=>{"name"=>"test"}}
Why are there two sets of parameters, one with the actual submitted data and one in the format of a hero object as if it was anticipating the mass assignment?
How come the require(:hero) doesn't raise an error when that key is not submitted? I assume the answer to this is because of what is automatically creating that second hash ("hero"=>{"name"=>"test"}} from question 1.
Any information on what I am missing here would be greatly appreciated, as this is barebones rails behavior out-of-the-box.
This behaviour comes from ActionController::ParamsWrapper:
Wraps the parameters hash into a nested hash. This will allow clients to submit requests without having to specify any root elements.
Rails applications have parameter wrapping activated by default for JSON requests. You can disable it globally by editing config/initializers/wrap_parameters.rb, or for individual controllers by including wrap_parameters false in the controller.
Is there any way to remove sensitive fields from the result set produced by the default ActiveRecord 'all', 'where', 'find', etc?
In a small project that I'm using to learn ruby I've a reference to User in every object, but for security reasons I don't want to expose the user's id. When I'm using a simple HTML response it is easy to remove the user_id simply by not using it. But for some task I'd like to return a json using something like:
def index
#my_objects = MyObject.all
respond_to do |format|
...
format.json { render json: #my_objects, ...}
...
end
end
How do I prevent user_id to be listed? Is there any way to create a helper that removes sensitive fields?
You can use the as_json to restrict the attributes serialized in the JSON response.
format.json { render json: #my_objects.as_json(only: [:id, :name]), ...}
If you want to make it the default, then simply override the method in the model itself
class MyObject
def serializable_hash(options = nil)
super((options || {}).merge(only: [:id, :name]))
end
end
Despite this approach is quick and effective, it rapidly becomes unmaintainable as soon as your app will become large enough to have several models and possibly different serialization for the same type of object.
That's why the best approach is to delegate the serialization to a serializer object. It's quite easy, but it will require some extra work to create the class.
The serializer is simply an object that returns an instance of a model, and returns a JSON-ready hash. There are several available libraries, or you can build your own.
Currently I have a rails controller with an update method:
def update
respond_to do |format|
if #segment.update(segment_params)
format.html { redirect_to #segment, notice: 'Segment was successfully updated.' }
else
format.json { render json: #segment.errors, status: :unprocessable_entity }
end
end
end
The segment_params method:
def segment_params
params.require(:segment).permit(:name, :description, :f_30_day_estimated_reach, :tags)
end
I'm unsure as to how to make the actual request to be accepted by rails. I'm using Postman (Google Chrome) to make the request. I'm making a PUT request to localhost:3000/segments/2677.json with the segment info as JSON. Is this the correct way of doing it? Or would this be a URL param? Not quite sure how to format it as a URL param.
You should not make changes to server data via a GET request. For the reasoning behind this see: http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html section 9.1.1 - bascally GET and HEAD request should always be safe.
The method you are using right now is actually a good one. My only suggeston - if you really want to follow the HTTP spec would be to use PATCH instead of PUT for partial updates. The semantics for PATCH are essentially that rather than being idempotent like PUT it does delta updates like what you are doing. See : http://restful-api-design.readthedocs.org/en/latest/methods.html for more info on the topic.
You can check the server log after the PUT and you'll see something like this:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"zzzz", "segment"=>{"name"=>"My Segment Name", "description"=>"Something", "f_30_estimated_reach"=> "I dont know", "tags"=> "tag1 tag2 tag3" }
The key "segment" must be present with your data as value. I assume that "tags" isn't an array, or represent an associated model.
If not, let say 'tags is an array', you must call permit this way:
params.require(:segment).permit(:name, :description, :f_30_day_estimated_reach, :tags [])
If tags is an associated model and you update the 'name' field:
params.require(:segment).permit(:name, :description, :f_30_day_estimated_reach, tags: {:name})
I hope it helps.
In Rails 4, the recommended HTTP action for an update action is the PATCH method. If you run rake routes in your Terminal, you'll see that the update action is linked to the PATCH action by default (assuming that you're using the resources method.
Rails is very opinionated about how you pass parameters in, and uses the concept of "strong params" to sanitize and validate your parameters as they're passed in from your form to your controller. This is a good primer on the topic (https://github.com/rails/strong_parameters), but I'm assuming you're trying to pass tags in as a collection (array in this case).
def segment_params
params.require(:segment).permit(:name, :description, :f_30_day_estimated_reach, :tags => [])
end
As Alejandro Babio said, if you're using an associated model then it you need to permit tags along with their model attributes that you've defined in your database migration file for the tags table. For example:
def segment_params
params.require(:segment).permit(:name, :description, :f_30_day_estimated_reach, :tags => {:name, :created_at, :updated_at})
end
It also might be a good idea to see what your params actually looks like by calling segment_params.inspect before #segment.update(segment_params). If :tags does not show up in the inspect method output, then it's not being passed into your strong params correctly.
Thanks for all the suggestions. This was an oversight on my part. I thought POSTMAN would specify the Content-Type HTTP Header as application/json because I was posting JSON.
I'm using RABL right now to generate JSON responses of an API in Rails, but I'm finding that while RABL is super handy for mapping models to responses, to create a consistent API I'm having to to duplicate that mapping logic in the update and create functions of my controller.
As a simple example, if I just want to change the attribute names in the response to a POST request, I can do this in RABL:
create.rabl
object #car
attributes car_id: :id, badly_named_legacy_column_that_means_color: :color
But if I want the client to be able to use these same "cleaned up" attributes in the JSON POST/PUT request itself (i.e. be able to send { "id": 1, "color": "red" } instead of { "car_id": 1, "badly_named_legacy_column_that_means_color": "red" }), I have to manually do this mapping again in the controller:
cars_controller.rb
def create
params[:car_id] = params.delete(:id)
params[:badly_named_legacy_column_that_means_color] = params.delete(:color)
#car = Car.create(params)
end
Now there are two places that I need to map car_id to badly_named_legacy_column_that_means_color. Not very DRY.
So far I haven't come across any way to handle this using RABL. Is there one that I'm missing? I also realize this might be outside the scope of RABL, which bills itself specifically as a templating system, so maybe is there another API builder that would allow me to do this? I love the idea of mapping messy database columns to a clean API but having to specify this mapping in both the view and the controller isn't very DRY. Any thoughts appreciated.
Update
The original answer is all about Ruby/Rails => JSON, the question is JSON => Ruby/Rails. This answer about associating columns should explain an approach:
alias_attribute :new_column_name, :column_name_in_db
Then you can just reference new_column_name in the RABL and Rails will handle the association on the create/update.
You should be able to call render from the create method and render any view. You could customize a response with a create specific template or reuse the generic show template. The trick is to re-use the object rabl template (app/views/car/car.rabl in this case), for example:
# POST /cars
def create
#car = Car.new(params)
if #car.save
render action: 'show'
else
respond_with #car
end
end
Where app/views/cars/car.rabl is
attributes :id, ...
and app/views/cars/show.rabl is
object #car
extends "cars/car"
By default calling rails.model.to_json
Will display something like this:
{"name":["can't be blank"],"email":["can't be blank"],"phone":["can't be blank"]}
Instead of message i need to generate some status code that could be used on service client:
[{"field": "name", "code": "blank"}, {"field": "email", "code": "blank"}]
This approach is very similar to github api v3 errors - http://developer.github.com/v3/
How can I achieve this with Rails?
On your model you can modify the way as json operates. For instance let us assume you have a ActiveRecord model Contact. You can override as_json to modify the rendering behavior.
def Contact < ActiveRecord::Base
def as_json
hash = super
hash.collect {|key, value|
{"field" => key, "code" => determine_code_from(value)}
}
end
end
Of course, you could also generate the json in a separate method on Contact or even in the controller. You would just have to alter your render method slightly.
render #contact.as_my_custom_json
In your controller, when you render the output, in your case JSON content, add the following :
render :json => #yourobject, :status => 422 # or whatever status you want.
Hope this helps