How can I customize Rails 3 validation error json output? - ruby-on-rails

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

Related

ActiveRecord nullifies non-existing values from request parameters

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.

How to pass multiple models to ActiveSerializer

My Rails controller action looks something as trivial as the following:
def show
#batter_rankings = DfsHittersBeta.all
#pitcher_rankings = DfsSpBeta.all
render :json => ??
end
In this case, both collections above each have their own serializer. I do want to have them as part of one API. So the API will ultimately look like:
{'pitchers' => [#pitcher_rankings],
'hitters' => [#hitter_rankings]
}
I'm not entirely sure how to pass both models to render as json each with their own serializer though and then perhaps a global serializer which allows me to specify how the final output looks.
You can include both pitchers and hitters in your JSON response like this:
render json: {pitchers: #pitcher_rankings, hitters: #hitter_rankings}

Rails rendering JSON data with Model Root

I've got some data in Rails that I want to render as JSON data. What I'm doing right now is simply finding all instances of a Model and calling render :json=>data.
data = Data.find(:all)
render :json => data
However, Rails is including the model name in each JSON object. So my JSON data ends up looking like this:
[{modelname:{propertyName: 'value',...}},{modelname:{propertyName: 'value2',...}}]
instead of this:
[{propertyName:'value',...},{propertyName:'value2',...}]
The modelname is always the same and I don't want it to be there.
I changed the option to render the root in the JSON data in one of the Rails initializers but that affects everything that I want rendered as JSON, which I don't want to do for this project.
In this case, I want to be able to do this on a case-by-case basis.
How can I do this? Thanks in advance.
With Rails 3, you can use active_model_serializers gem1
that allows you to specify rootless rendering of an object like this:
render :json => data, :root => false
I did not find a way to do this by passing options to the to_json method (and I don't believe there is such an option). You have more alternative to do this, any class that inherits from ActiveRecord::Base will have include_root_in_json.
Do something like this.
Data.include_root_in_json = false
data = Data.find(:all)
render :json => data
Hope this gets you going.
Ok let's try this then.
DataController < ApplicationControlle
private
def custom_json(data)
Data.include_root_in_json = false
data.to_json
Data.include_root_in_json = true
data
end
end
Then your redirect would look like this
data = Data.find(:all)
render :json => custom_json(data)
It's pretty silly code I wish I could think of something else entirely. Let me ask you this: What is it about having the model name included in the json data ?
With Rails 3, I found this way better to do. Override the as_json in your model and do as follows:
def as_json(options = {})
super(options.merge :methods => [:some_method_that_you_want_to_include_result], :include => {:child_relation => {:include => :grand_child_relation } })
end

How to override to_json in Rails?

Update:
This issue was not properly explored. The real issue lies within render :json.
The first code paste in the original question will yield the expected result. However, there is still a caveat. See this example:
render :json => current_user
is NOT the same as
render :json => current_user.to_json
That is, render :json will not automatically call the to_json method associated with the User object. In fact, if to_json is being overridden on the User model, render :json => #user will generate the ArgumentError described below.
summary
# works if User#to_json is not overridden
render :json => current_user
# If User#to_json is overridden, User requires explicit call
render :json => current_user.to_json
This all seems silly to me. This seems to be telling me that render is not actually calling Model#to_json when type :json is specified. Can someone explain what's really going on here?
Any genii that can help me with this can likely answer my other question: How to build a JSON response by combining #foo.to_json(options) and #bars.to_json(options) in Rails
Original Question:
I've seen some other examples on SO, but I none do what I'm looking for.
I'm trying:
class User < ActiveRecord::Base
# this actually works! (see update summary above)
def to_json
super(:only => :username, :methods => [:foo, :bar])
end
end
I'm getting ArgumentError: wrong number of arguments (1 for 0) in
/usr/lib/ruby/gems/1.9.1/gems/activesupport-2.3.5/lib/active_support/json/encoders/object.rb:4:in `to_json
Any ideas?
You are getting ArgumentError: wrong number of arguments (1 for 0) because to_json needs to be overridden with one parameter, the options hash.
def to_json(options)
...
end
Longer explanation of to_json, as_json, and rendering:
In ActiveSupport 2.3.3, as_json was added to address issues like the one you have encountered. The creation of the json should be separate from the rendering of the json.
Now, anytime to_json is called on an object, as_json is invoked to create the data structure, and then that hash is encoded as a JSON string using ActiveSupport::json.encode. This happens for all types: object, numeric, date, string, etc (see the ActiveSupport code).
ActiveRecord objects behave the same way. There is a default as_json implementation that creates a hash that includes all the model's attributes. You should override as_json in your Model to create the JSON structure you want. as_json, just like the old to_json, takes an option hash where you can specify attributes and methods to include declaratively.
def as_json(options)
# this example ignores the user's options
super(:only => [:email, :handle])
end
In your controller, render :json => o can accept a string or an object. If it's a string, it's passed through as the response body, if it's an object, to_json is called, which triggers as_json as explained above.
So, as long as your models are properly represented with as_json overrides (or not), your controller code to display one model should look like this:
format.json { render :json => #user }
The moral of the story is: Avoid calling to_json directly, allow render to do that for you. If you need to tweak the JSON output, call as_json.
format.json { render :json =>
#user.as_json(:only => [:username], :methods => [:avatar]) }
If you're having issues with this in Rails 3, override serializable_hash instead of as_json. This will get your XML formatting for free too :)
For people who don't want to ignore users options but also add their's:
def as_json(options)
# this example DOES NOT ignore the user's options
super({:only => [:email, :handle]}.merge(options))
end
Hope this helps anyone :)
Override not to_json, but as_json.
And from as_json call what you want:
Try this:
def as_json
{ :username => username, :foo => foo, :bar => bar }
end

Pass method chains to to_json

I know you can pass methods the values of which you want to be available to json objects like so:
# user.rb
def name
first_name + last_name
end
# some controller
render :json => #user.to_json(:methods => :name)
But if I want to massage the value returned from the method a bit (with a text helper say) is there a way to do that? I guess another way to ask this is does #to_json support arbitrary attributes? If not, why not? Has anyone else ran into this before?
You can use "render :json" to specify arbitrary attributes in the JSON output. Here is an example:
render :json => { :arbitraryAttribute => arbitrary_method_to_call(), :user => #user.to_json }
The above code would generate JSON like the following:
{
"arbitraryAttribute":"returnValueOfMethodCall",
"user":{ the result of #user.to_json }
}

Resources