Rails JSON API layouts with Jbuilder (or other) - ruby-on-rails

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

Related

Using jBuilder to build a complex array

I am using jBuilder and jsTree (https://www.jstree.com/docs/json/) in my Rails app and trying to build an array like this:
[
{
id : "string" // required
parent : "string" // required
text : "string" // node text
icon : "string" // string for custom
state : {
opened : boolean // is the node open
disabled : boolean // is the node disabled
selected : boolean // is the node selected
},
li_attr : {} // attributes for the generated LI node
a_attr : {} // attributes for the generated A node
},
{...},
{...}
]
I did this before with a simple json.array! and a do loop with a set of results from my database. No problems there. The issue is that I have polymorphic parents i.e. there are different models. I will equate this to an example where I have a 'Products' and 'Equipment' and they all have comments nested below. I want to list all the Projects (with child comments) then list all the Equipment and then child comments for them. I essentially need a loop like this:
[
projects do |p|
json.id id
json.parent "#"
...
end
equipment do |e|
json.id id
json.parent "#"
...
end
comments do |c|
json.id id
json.parent c.parent_id
...
end
]
This way I can build the hash of data for jsTree to parse. The docs for jBuilder are not great and not sure how or of I can do this.
I would just skip jBuilder. Its slow as heck and do you really need a super awkward DSL to build JSON objects? After all Rubys hashes and arrays map cleanly to JSON types.
class JsTreeSerializer
# #param [Array] records
# #param [Mixed] context - used to call view helpers
def initialize(records, context: nil)
#records = records
#context = context
end
def serialize
json = #records.map do |record|
{
id: record.id,
parent: record.parent_id,
icon: context.image_url('icon.gif')
# ...
}
end
end
end
Usage:
class MyController
def index
#records = get_all_the_types
respond_to do |f|
format.json do
render json: JsTreeSerializer.new(
#records,
context: view_context
).serialize
end
end
end
end
It ended up being as simple as:
json.array!
json.(#projects) do |p|
json.id id
json.parent "#"
...
end
json.(#equipment) do |e|
json.id id
json.parent "#"
...
end
json.(#comments) do |c|
json.id id
json.parent c.parent_id
...
end

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.

How to customize the JSON that respond_with renders in case of validation errors?

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"
}

Serialize an array of models using active_model_serializers

I am trying to send the serialized version of a model to a view as a param, using the gem active_model_serializers
#app/serializers/admin_serializer.rb
class AdminSerializer < ActiveModel::Serializer
attributes :id, :email, :access_locked?
end
#app/controllers/dashboard/admins_controller.rb
def index
#search = Admin.search(params[:q])
#admins = #search.result(:distinct => true).page(params[:page]).per(10)
#page_entries_info = view_context.page_entries_info #admins
# render json: #admins
respond_to do |format|
format.html
format.js
format.json {render json: #admins}
end
end
#app/views/dashboard/admins/index.html.erb
<%= debug (ActiveModel::Serializer::Adapter.adapter_class(:json_api).new(ActiveModel::Serializer.serializer_for(#admins.first).new(#admins.first),{}).to_json) %>
<%= debug (#admins.all.map{|admin| AdminSerializer.new(admin).to_json}) %>
Above debugs are yielding the below response:
--- '{"data":{"id":"1","type":"admins","attributes":{"email":"tech#bluesapling.com","access_locked?":false}}}' //returned by the first debug
---
- '{"object":{"id":36,"email":"aubrey_schmitt#feeneykoch.io","created_at":"2016-03-28T05:15:17.546Z","updated_at":"2016-03-28T05:15:17.546Z"},"instance_options":{},"root":null,"scope":null}'
- '{"object":{"id":20,"email":"alysa_johnston#thompson.io","created_at":"2016-03-28T05:15:16.304Z","updated_at":"2016-03-28T05:15:16.304Z"},"instance_options":{},"root":null,"scope":null}'
- '{"object":{"id":22,"email":"kristofer.langosh#kunzeluettgen.com","created_at":"2016-03-28T05:15:16.459Z","updated_at":"2016-03-28T05:15:16.459Z"},"instance_options":{},"root":null,"scope":null}'
- '{"object":{"id":37,"email":"beryl_keler#wiza.biz","created_at":"2016-03-28T05:15:17.624Z","updated_at":"2016-03-28T05:15:17.624Z"},"instance_options":{},"root":null,"scope":null}'
- '{"object":{"id":5,"email":"wilhelmine_buckridge#crona.io","created_at":"2016-03-28T05:15:15.139Z","updated_at":"2016-03-28T05:15:15.139Z"},"instance_options":{},"root":null,"scope":null}'
- '{"object":{"id":14,"email":"edward_wisoky#corkery.net","created_at":"2016-03-28T05:15:15.838Z","updated_at":"2016-03-28T05:15:15.838Z"},"instance_options":{},"root":null,"scope":null}'
- '{"object":{"id":27,"email":"leonor#jerde.biz","created_at":"2016-03-28T05:15:16.848Z","updated_at":"2016-03-28T05:15:16.848Z"},"instance_options":{},"root":null,"scope":null}'
- '{"object":{"id":2,"email":"carley#wyman.net","created_at":"2016-03-28T05:15:14.873Z","updated_at":"2016-03-28T05:15:14.873Z"},"instance_options":{},"root":null,"scope":null}'
- '{"object":{"id":10,"email":"ervin.gleichner#cremin.org","created_at":"2016-03-28T05:15:15.527Z","updated_at":"2016-03-28T05:15:15.527Z"},"instance_options":{},"root":null,"scope":null}'
- '{"object":{"id":15,"email":"lonzo.dickens#johnscole.name","created_at":"2016-03-28T05:15:15.916Z","updated_at":"2016-03-28T05:15:15.916Z"},"instance_options":{},"root":null,"scope":null}'
In the first debug I am serializing only one object, while in the second one I am trying to do it for an array of objects.
The first debug is correctly returning the serialized version of the object(in json_api format) while second debug is not.
Tried ArraySerializer as well, with no success: ActiveModel::Serializer::ArraySerializer.new(#admins, each_serializer: AdminSerializer).as_json
how do I achieve the desired serialization. Moreover, if achieved, can I used some other simplified version of this? As this debug statement is way too verbose.
Tried all the solutions mentioned here - How do you initialize an ActiveModel::Serializer class with an ActiveRecord::Relation array?
The basic problem which I am trying to solve is, in the index method of the Admin controller, the Admin object is passed as a PORO to the index.html file. But I want the serialized json version of this object so that I can pass it to my react components as a prop
index method is rendering proper json on firing http://dashboard.localhost.com:3000/admins.json
UPDATE#1 for the index method
def index
#search = Admin.search(params[:q])
#admins_array = #search.result(:distinct => true).to_a
if params[:page]
#admins = #search.result(:distinct => true).page(params[:page][:number]).per(10)
#admins_json_array = Kaminari.paginate_array(#admins_array).page(params[:page][:number]).per(10)
else
#admins = #search.result(:distinct => true).page(1).per(10)
#admins_json_array = Kaminari.paginate_array(#admins_array).page(1).per(10)
end
#admins_json = ActiveModel::SerializableResource.new(#admins_json_array.to_a)
...
...
...
end
I have a controller that I need to specify the serializer in, due to wanting different attributes from the default serializer.
In Controller:
def index
search = User.ransack(search_params)
render json: search.result, each_serializer: MembershipRenewalSerializer::MemberSerializer
end
So, just to get things working, what happens if you specify the each_serializer option?
Edits:
Outside Controller:
ActiveModel::SerializableResource.new(
User.first(2),
each_serializer: MembershipRenewalSerializer::MemberSerializer
).to_json
Note, that without specifying each_serializer, SerializableResource would use the UserSerializer.
Edit #2,
It looks like there is something weird happening with the #admins data.
Try converting to an array:
ActiveModel::SerializableResource.new(#admins.to_a).to_json
Edit #3
To paginate your array, try the following:
#search = Admin.search(params[:q])
#results = #search.result(:distinct => true).to_a
#admins = Kaminari.paginate_array(#results).page(params[:page]).per(10)
Follow the guide: Serializing before controller render
You could use ActiveModel::SerializableResource.new(#admins, adapter: :json_api).to_json
in index.html.erb
<%= debug (ActiveModel::SerializableResource.new(#posts, adapter: :json_api).to_json) %>
below is the output(using posts)
'{"data":[{"id":"1","type":"posts","attributes":{"title":"first post","body":null}},{"id":"2","type":"posts","attributes":{"title":"second post","body":null}}],"links":{}}
I create a concern with some API helper methods and there you can check if its a collection pass find the appropiate serializer and pass it to the collection serializer.
def api_response(data)
render json: wrap_answer(data)
end
def wrap_answer(data)
if data.respond_to?(:each)
ActiveModel::Serializer::CollectionSerializer.new(data, each_serializer: ActiveModel::Serializer.serializer_for(data.first))
else
data
end
end
should have made that into string and use json.stringify to make that as a string and make your life easy

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

Resources