How to customize the JSON that respond_with renders in case of validation errors? - ruby-on-rails

In a controller, I want to replace if..render..else..render with respond_with:
# Current implementation (unwanted)
def create
#product = Product.create(product_params)
if #product.errors.empty?
render json: #product
else
render json: { message: #product.errors.full_messages.to_sentence }
end
end
# Desired implementation (wanted!)
def create
#product = Product.create(product_params)
respond_with(#product)
end
The problem with respond_with is that, in case of a validation error, the JSON renders in a specific way that doesn't fit what the client application expects:
# What the client application expects:
{
"message": "Price must be greater than 0 and name can't be blank"
}
# What respond_with delivers (unwanted):
{
"errors": {
"price": [
"must be greater than 0"
],
"name": [
"can't be blank"
]
}
}
Product, price and name are examples. I want this behavior through the entire application.
I am using the responders gem and I've read it's possible to customize responders and serializers. But how do the pieces fit together?
How to customize the JSON that respond_with renders in case of validation errors?

A couple other ways to customize user alerts
You can just put it in line:
render json: { message: "Price must be greater than 0" }
or: You can just reference your [locale file] and put in custom messages there. 1:
t(:message)
Hope this helps :)

I found a provisory way to get the errors hash as a single sentence. But not only is it hackish, but it also does not match the desired output 100%. I still hope there is a way to do this with a custom serializer or responder.
module ActiveModel
class Errors
def as_json(*args)
full_messages.to_sentence
end
end
end
# OUTPUT
{
"errors": "Price must be greater than 0 and name can't be blank"
}

Related

How to return JSON from Rails with camelcased key names

I'm building a JS app with a Rails backend and in order not to confuse snake and camel cases, I want to normalize it all by returning camelcase key names from the server. So user.last_name would return user.lastName when returned from the API.
How do I achieve this? Thanks!
Edit: Added Controller Code
class Api::V1::UsersController < API::V1::BaseController
# authorize_resource
respond_to :json, only: [:index]
def sky
#user = User.find_by_id(params[:user_id])
if #user
obj = {
sky: {
sectors: #user.sectors,
slots: #user.slots
}
}
render json: obj
else
raise "Unable to get Sky"
end
end
end
The way I do it is using ActiveModelSerializer and the json_api adapter:
In your Gemfile, add:
gem 'active_model_serializers'
Create a new file /config/initializers/ams.rb containing:
ActiveModelSerializers.config.adapter = :json_api
ActiveModelSerializers.config.key_transform = :camel_lower
Your controller action should look like this:
class ApiController < ApplicationController
def sky
#user = User.find_by_id(params[:user_id])
if #user
render json: #user, serializer: UserSkySerializer
else
raise "Unable to get Sky"
end
end
end
Now you need to create a serializer. First, create a new directory app/serializers/.
Next, create a new serializer app/serializers/user_sky_serializer.rb, containing:
class UserSkySerializer < ActiveModel::Serializer
attributes :sectors, :slots
end
The result will be similar to what you describe in your obj hash, but all attribute keys will be rendered in camelCase using the jsonapi standard.
Another option is the use the olive_branch gem. As described in this post, all you need to do is:
Add this gem
add config.middleware.use OliveBranch::Middleware in application.rb.
Then, add this header to requests from your client-side app:
'X-Key-Inflection': 'camel'
If you are using libs like axios, you can add this header to a constant along with other headers:
const HEADERS = {
...
'X-Key-Inflection': 'camel'
}
const request = axios.post(url, param, HEADERS)
This way you don't need to deep_transform keys manually on the server side. Even deeply-nested json keys will be camelized. Example response from such request:
[
{
"id": 1,
"firstName": "Foo",
"lastName": "Bar",
"createdAt": ...,
"updatedAt": ...,
...
"cabinAssignments": [
{
"id": 1,
"cabinKeeperId": 1,
"userId": 1,
"houseId": 1,
...
}
]
}
]
You don't mention your Rails version, so for others who look for this, I'll mention that in Rails 5 the answer is to use #camelize from ActiveSupport::Inflector. In my case, I had to call it in a chain on an ActiveRecord model (t below):
def index
trucks = AvailableTruck.all.map do |t|
t.as_json(only: AvailableTruck.exposed)
.deep_transform_keys(&:camelize).deep_transform_values(&:upcase)
end
render json: trucks
end
#deep_transform_keys may or may not be necessary depending on your application.
You can achieve that with ruby's method_missing method.
Create a file something like concerns/method_missing_extension.rb and put following code in it.
module MethodMissingExtension
def method_missing(method_name, args = {})
if self.class.column_names.include? method_name.to_s.underscore
send method_name.to_s.underscore.to_sym
else
super
end
end
end
Include this module in each model like include MethodMissingExtension.
Now whenever you do user.firstName then user.first_name will returned.

Printing error when using PARAMS in Rails

For my API in RAILS I have programmed a code that basically does the following.
class Api::V1::NameController < ApplicationController
skip_before_filter :verify_authenticity_token
def index
end
def create
# Loading data
data_1_W = params[:data1]
data_2_W = params[:data2]
while len > i
# -Here I do some calculations with data_1_W and data_2_W.
# Its not important to show the code here
end
# -Organizing outputs to obtain only one JSON-
# Its not important to show the code here
# Finally HTTP responses
if check_error == 1
render status: 200, json: {
message: "Succesful data calculation",
data_output: response_hash
}.to_json
end
end
end
To test that everything is working I use the cURL command. I notice that loading the data could be a problem and therefore the code would break.
I want to tell the user that it was an error loading the data for some reason (HTTP response), but I don't know where to put it. If I put and else under my success status it would not print it because the code breaks just starting (instead of sending the correct name - d '#data.json' of the data in cURL I send -d '#dat.json').
The data I am loading is a JSON data {"data1":[{"name1":"value1"},{"name2":number2}...],"data2":[{"name1":"value1"},{"name2":number2...}]}. (This data has 70080 rows with 2 columns if we see it as a table, which I divided into two in my CODE for calculations purposes data_1_W and data_2_W)
Could anyone help me where to put it? more or less like this:
render status: 500, json: {
message: "Error loading the data",
}.to_json
Put it in a rescue block around the code that throws the error.
E.g.
def func
# code that raises exception
rescue SomeException => e
# render 422
end
Since you are working in Rails I'd recommend going the rails way. This means that I would create some kind of service and initialize it in the create action.
Now, within the service you do all you funky stuff (which also allows you to clean this controller and make i look prettier) and the moment a condition is not fulfilled in that service return false. So...
# controllers/api/v1/name_controller.rb
...
def create
meaningful_variable_name = YourFunkyService.new(args)
if meaningful_variable_name.perform # since you are in create then I assume you're creating some kind of resource
#do something
else
render json: {
error: "Your error",
status: error_code, # I don't think you want to return 500. Since you're the one handling it
}
end
end
# services/api/v1/your_funky_service.rb
class Api::V1::YourFunkyService
def initiliaze(params)
#params = params
end
def perfom #call it save if you wish
....
return false if check_error == 1
end
end

How can I get my rails controller (CREATE) to accept/POST multiples JSON records?

I have a backend Rails JSON API. Right now, I am only able to create one record per POST based on the way I've coded my controller.
Let's say we have an application where Users can create To-Do Lists and in those lists they can create items.
3 models for our example, User, UserList, and UserListItems
User has_many UserLists
UserLists has_many UserListItems
Now right now, I'm updating UserListItems with a POST, but I can only add one item at a time. The JSON looks like...
{
"user_list_item":
{
"item_title": "Buy Milk"
}
}
And using Active Model Serializers, I am returning the record that it creates, and it looks as follows...
respond_with :api, :v1, #current_user, user_list, user_list_item, location: nil, serializer: Api::V1::UserListItemSerializer
{
"user_list_item": {
"id": 11,
"user_list_id": 2,
"item_title": "Buy Milk"
}
}
There is a serious flaw with this. If a User creates a 'Grocery To-Do List' and we POST 1 UserList record... that's fine. But then they might begin to fill the grocery list and add 3 things at once. "Buy Milk", "Buy Eggs", "Get Gas". We now need to POST to UserItemList 3 times, and that's far from ideal. What if they added 20 items. We should aim to add all 20 in 1 POST.
How can I do this?!?
1) Can someone show me the entire sample code for the CREATE in the controller to do this, as I learn best by seeing/doing syntax. I want to be able to pass in...
{
"user_list_item":
[
{
"item_title": "Buy Milk"
},
{
"item_title": "Buy Eggs"
},
{
"item_title": "Get Gas"
}
]
}
and have my controller parse, loop, and create them. 2) And I also want to be able to return the record(s) to the creator via Active Model Serializers. So for example, all 3 of these newly added records in one return (mostly I'm just interested in the first thing right now though).
EDIT: Adding my original controller#create code
if authenticate_user
user_list = #current_user.user_lists.find_by_id(params[:user_list_id])
user_list_item = user_list.user_list_items.new(user_list_item_params)
if (user_list_item.save!)
respond_with :api, :v1, #current_user, user_list, user_list_item, location: nil, serializer: Api::V1::UserListItemSerializer
else
render json: { error: "Could not create new User List Item."}, status: :unprocessable_entity
end
else
render json: { error: "User is not signed in." }, status: :unauthorized
end
private
def user_list_item_params
params.require(:user_list_item).permit(:user_list_id, :title, :due_date)
end
Now, with #Arvoreniad's suggestion below, I now have the following...
if authenticate_user
#user_list = #current_user.user_lists.find_by_id(params[:user_list_id])
#error = false
user_list_item_params[:user_list_item].each do |attribute|
item = #user_list.user_list_items.new
item.title = attribute.title
item.due_date = attribute.due_date
#error = true unless item.save
end
if (error)
????????????????
else
render json: { error: "Could not create new User List Item(s)."}, status: :unprocessable_entity
end
private
def user_list_item_params
params.require(:user_list_item).permit(:user_list_id, :title, :due_date)
end
My new questions are
1) Where all the ?'s are... where I was previously using ActiveModelSerializers to show the newly added record, how can I return something showing all the newly added records? Or this that not typical of an API? Is it just common to return something empty and just go off of whether it was successful or not?
2) Can-I/Should-I create both things in 1 POST (UserList and Multiple UserListItem's all in 1 POST)? If so, how?
3) How can I rollback all saves if one does not work. Let's say I'm trying to add 10 items, and 9 succeed but 1 fails, how do I roll them all back?
Your question is a little difficult to answer without your previous controller code, but this should give you a starting point based on the info you supplied. EDIT: Added the code for points 1 and 3.
If you want to create a UserList in the same action, just post the required data and create one from the parameters, rather than finding it in the database. I can help you with this if you'd like, but it shouldn't be too hard.
def create
#user_list = UserList.find(params[:id)
#error = false # Use this to check before rendering if there has been an error.
params[:user_list_item].each do |attributes|
item = UserListItem.new
item.item_title = attributes.item_title
item.user_list_id = #user_list.id
#error = true unless item.valid?
#items << item
end
if(#error)
render json: { error: "Could not create new User List Item(s)."}, status: :unprocessable_entity
else
#items.each do |item|
item.save
end
serialized = ActiveModel::ArraySerializer.new(#items, each_serializer: UserListItemSerializer)
render json: serialized
end
end

Rails: validation error codes in JSON

So, I am writing Rails web application which has JSON API for mobile apps. For example, it sends POST JSON request to example.com/api/orders to create order.
{id: 1, order: { product_name: "Pizza", price: 10000}}
In case of validation errors I can response with HTTP 422 error code and order.errors.full_messages in json. But it seems better for me to have specific error code in JSON response. Unfortunately, it seems like Rails does not provide ability to set error code for validation error. How to solve this problem?
You can pass a custom status code by using the status option when rendering the response.
def create
#order = ...
if #order.save
render json: #order
else
render json: { message: "Validation failed", errors: #order.errors }, status: 400
end
end
I usually tend to return HTTP 400 on validation errors. The message is a readable status response, the errors are also attached.
This is a respons example
{
message: "Validation failed",
errors: [
...
]
}
You can also embed additional attributes.
I was after something similar, so what I did was extend String eg
class ErrorCodeString < String
def init(value, error_code)
#error_code = error_code
super(value)
end
def error_code
#error_code
end
end
Then in a custom validation (this won't work on standard validation) I'd do
errors.add(:email, ErrorCodeString.new('cannot be blank', 50)
Now when you return your JSON you can check to see if the error value is an ErrorCodeString and add the error_code value to the output. As ErrorString inherits String, you shouldn't be breaking anything else along the way.
Rails 5 has error.details that can be used for exactly that.
In the model
errors.add(:price, 1023, message: "Transaction value #{price} is above limit (#{maximum_price}).")
In the controller
format.json { render json: #order.errors.details, status: :unprocessable_entity }
error details can be anything, eg. you could also use :above_limit instead of 1023.
The API response body will then look like
pp JSON.parse(response)
{"price"=>[{"error"=>1023}]}
This feature has been backported to Rails 4, see also http://blog.bigbinary.com/2016/05/03/rails-5-adds-a-way-to-get-information-about-types-of-failed-validations.html
Also: Is there a way to return error code in addition to error message in rails active record validation?

Rails JSON API layouts with Jbuilder (or other)

In my rails 3.2 app, I'm using jbuilder to render responses from my JSON api.
I want to provide a common structure to all API responses, and a layout would be the likely solution to keep my views DRY.
ex: I'd like every response to be of the following form :
{
status: "ok|error|redirect",
data: { ... JSON specific to the current view ... },
errors: [ ... ],
notes: [ ... ]
}
(where the value for data is a json structure provided by the view, everything else is from the layout)
However: I can't get the jbuilder layout yielding to the view correctly.
# in layout
json.data yield
# in view
json.some "value"
results in:
{"data":"{\"some\":\"value\"}"} # arg! my json has become a string
Trying things another way:
# in layout
yield
# in view
json.data do |json|
json.some "value"
end
results in :
{}
Has anyone had success doing this with jbuilder, or another json templating gem/method?
This juilder github issue suggests it's possible, but indicates others are having similar issues.
I see rabl (https://github.com/nesquena/rabl/) is supposed to support layouts (https://github.com/nesquena/rabl/wiki/Using-Layouts), but I've decided not to use it for other reasons (rabl makes complex json structures a nightmare, particularly when trying to control object roots etc).
I'll give you an alternative based on a solution we came up with:
# app/helpers/application_helper.rb
module ApplicationHelper
def envelope(json, status, errors, notes)
json.status status
json.data do
yield if block_given?
end
json.errors errors
json.notes notes
end
end
then, in the view, you can call envelope and include your json code like:
# app/views/api/v1/test/show.json.jbuilder
envelope(json, "OK" ) do
json.some 'value'
end
You can do this by this way
# api.v1.json.jbuilder - layout
json.request do
json.message "your message"
json.status 200
end
json.data JSON.parse(yield)
# show.json.jbuilder - action view
json.name 'Some item name'
Late answer, but helped me get what I was looking for...
Success Result:
{ "something": {"id": 42, "message": "hello"}, "status": "ok", "errors": [] }
Error Result:
{ "something": null, "status": "error", "errors": ["could not do the thing"] }
Code:
app/controllers/api/v1/base_controller.rb
class Api::V1::BaseController < ActionController::API
layout 'api/v1/application'
before_action :setup_layout_elements
def setup_layout_elements
#status = :ok
#errors = []
end
def error!(message)
#status = :error
#errors << message
nil
end
end
app/controllers/api/v1/some_controller.rb
class Api::V1::SomeController < Api::V1::BaseController
def index
#something = begin
possibly_error_causing_code
rescue
error!('could not do the thing')
end
render builder: 'api/v1/something/index'
end
end
app/views/layouts/api/v1/application.json.jbuilder
json.merge! JSON.parse(yield)
json.status #status
json.errors #errors
app/views/api/v1/something/index.json.jbuilder
json.something do
json.id #something.id
json.message #something.to_s
end
Try
json.merge! JSON.parse(yield)
https://github.com/rails/jbuilder/issues/8#issuecomment-27586784
JBuilder does not support using json.jbuilder as your layout (see issue #172 on Github).
I managed to avoid doing an extra round of parse&generate by using json.erb as my layout format.
app/controllers/api/base_controller.rb:
class Api::BaseController < ActionController::Base
layout "api.v1"
end
app/views/layouts/api.v1.json.erb:
{
<% if #api_errors.present? %>
"errors": <%= raw JSON.dump #api_errors %>,
<% else %>
"data": <%= yield %>,
<% end %>
"meta": <%= raw JSON.dump #api_meta %>
}
In case you don't want to include extra key you can do so
class UsersController < ApplicationController
layout: 'json_layout'
end
In /app/views/layouts/json_layout.json.jbuilder
json.success true
r = JSON.parse(yield)
r.each{|k,v|
json.set! k,v
}
jbuilder is pretty simple technique for API views here you can add partials so if you want the same response for all the API create a decorator or create partial for the common response and call that response where ever you need that
Lets say if you want
{
status: "ok|error|redirect",
data: { ... JSON specific to the current view ... },
errors: [ ... ],
notes: [ ... ]
}
create a partial for this
/views/api/common/_some_partial
json.status "ok whatever the message"
json.data do
json.message "message"
end
json.errors #errors
json.notes #notes_array
Its pretty much simple solution for your question.
Cheers

Resources