11

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).

MikeL
  • 483
  • 2
  • 6
  • 12

7 Answers7

15

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
sorens
  • 4,975
  • 3
  • 29
  • 52
  • You have `do` missing after `envelope(json, "OK" )` in the view code – JVK Aug 13 '13 at 06:22
  • any idea about http://stackoverflow.com/questions/18202750/rails4-jbuilder-always-return-status-code-200-even-if-i-put-other-status-code – JVK Aug 13 '13 at 07:19
11

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'
Dobromir Minchev
  • 367
  • 5
  • 10
  • 2
    I like where you're going with this but it seems somewhat wasteful to parse the JSON only to spit it straight back out again – Peter Nixey Jun 09 '14 at 18:31
  • Peter: I've been doing it this way, and it works, but it's a massive kludge. And the JSON.parse causes problems with some views when it "intelligently" converts data types. e.g., some strings are being turned into numbers. – Glenn Oct 23 '18 at 20:50
11

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
Chris Cashwell
  • 22,308
  • 13
  • 63
  • 94
2

Try

 json.merge! JSON.parse(yield)

https://github.com/rails/jbuilder/issues/8#issuecomment-27586784

Ameen
  • 84
  • 2
2

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 %>
}
Adam Wróbel
  • 125
  • 4
1

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
}
Hrishabh Gupta
  • 1,912
  • 15
  • 13
-2

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

Ali Hassan Mirza
  • 552
  • 11
  • 23