04 May 2016

Setting up JSON API in Rails 5 beta

Request:

curl -X POST \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -d '{
  "data": {
    "type": "sandwiches",
    "attributes":
    {
      "spreads": [ "jelly", "peanut_butter" ],
      "toasted": false,
      "customer_id": "42"
    }
  }
}' "https://site.dev/sandwiches?authToken=abcd1234"

Response:

HTTP/1.1 201 Created
Content-Type: application/vnd.api+json

{
  "data": {
    "id":   "1337",
    "type": "sandwiches",
    "attributes": {
      "spreads": [ "jelly", "peanut_butter" ],
      "toasted": false
    },
    "relationships": {
      "customers": {
        "data": { "id": "42", "type": "customers" }
      }
    }
  }
}

or

HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json

{
  "errors": [
    {
      "source": {
        "pointer": "/data/attributes/customer_id"
      },
      "detail": "Customer with id 42 does not exist"
    },
    {
      "source": {
        "pointer": "/data/attributes/spreads"
      },
      "detail": "jelly is not a valid spread"
    }
  ]
}

Content Negotiation

curl -X POST \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json"

Means that the media type of the request payload is JSON API and the client will only Accept a JSON API response.

JSON API is not the same as JSON: https://github.com/rails/rails/pull/21496

Handling a JSON API request

Rails returns {} for an unknown Content-Type:

# actionpack/test/dispatch/request/json_params_parsing_test.rb

test "does not parse unregistered media types such as application/vnd.api+json" do
  assert_parses({}, "{\"person\": {\"name\": \"David\"}}", { 'CONTENT_TYPE' => 'application/vnd.api+json' })
end

So, we register the JSON API media type in an initializer:

# actionpack/lib/action_dispatch/http/mime_types.rb

MEDIA_TYPE = 'application/vnd.api+json'.freeze
Mime::Type.register(MEDIA_TYPE, :jsonapi)

Now Rails knows about Mime[:jsonapi] but needs to learn how to parse params.

Where our payload:

{
  "data": {
    "type": "sandwiches",
    "attributes":
    {
      "spreads": [ "jelly", "peanut_butter" ],
      "toasted": false,
      "customer_id": "42"
    }
  }
}

must be parsed to a format models and records can use.

{ sandwiches: { spreads: ["jelly", "peanut_butter" ], toasted: false, customer_id: "42" }

Now configure JSON API request payloads to be parsed by the (currently) non-existent JSONAPI gem.

# actionpack/lib/action_dispatch/http/parameters.rb:79 in params_parsers

parsers = (Rails::VERSION::MAJOR >= 5 ? ActionDispatch::Http::Parameters : ActionDispatch::ParamsParser)::DEFAULT_PARSERS
parsers[Mime[:jsonapi]] = ->(body) { JSONAPI.parse(body).with_indifferent_access }

Rendering a JSON API response

In our controller, we’re going to have a create method that looks like:

def create
  sandwich = Sandwich.new(create_params)
  if sandwich.save
    render jsonapi: sandwich, status: :created
  else
    render jsonapi: sandwich_errors, status: :bad_request
  end

Notice that jsonapi? That’s a Renderer we need to add in order to render our response in the JSONAPI format.


# "rails-40f68f72fc11/actionpack/lib/action_controller/metal/renderers.rb
ActionController::Renderers.add :jsonapi do |json, options|
  json = json.is_a?(String) ? json : JSONAPI.dump(json, options)

  if options[:callback].present?
    if content_type.nil? || content_type == Mime[:jsonapi]
      self.content_type = Mime[:js]
    end

    "/**/#{options[:callback]}(#{json})"
  else
    self.content_type ||= Mime[:jsonapi]
    json
  end
end

References:



blog comments powered by Disqus