There's one great thing about the way Han Solo was "packaged" for transportation to Jabba the Hutt. You see, once you freeze someone in carbonite, you make it very clear and explicit as to your intentions towards the poor guy. Everyone can see it without thinking twice and nobody will even dare to ask silly questions about the package format or its recipient.
This transparency and explicitness, although without the unnecessary glimpse of terror, was the reason for creating yet another JSON API wrapper package, one that would fit the Elixir/Phoenix ecosystems which on their own also promote explicitness and simplicity over DSLs and magic.
So here is the JSON API Building Blocks Assembly for Elixir. Use it, as Jabba once did, to set a brutally clear example as to how explicit and readable your JSON API view code should be.
As opposed to the DSL-powered serializer approach, Jabbax tries to stay away from your view layer logic. It simply provides you with all the building blocks that you'll need to assemble a complete JSON API document, structured to resemble the actual JSON API document structure. This has the following advantages:
- your view code works on ("talks in the language of") the real JSON API structure, just as HTML, JSON or any other views are meant express structures of the relevant standard with Elixir code
- you have the flexibility to abstract away common or repetitive parts into any code structures that you choose as a best fit for your business domain and project specifics
- your view code is explicit and easy to reason about, because each document is either explicitly assembled in the view or explicitly abstracted away into separate functions or modules
- you'll never hit the wall of an edge case scenario unsupported by serialization DSL, such as a need to include deeply nested structures that are not following the standard Ecto structure etc
Once you assemble the document, Jabbax takes it from there and serializes (or deserializes) the
Jabbax.Document
structure to (or from) a JSON string. At this phase, Jabbax does provide a few
basic conveniences to make sure your output strictly follows the JSON API
requirements and recommendations without you having to care for some minor details. But other than
than, everything is under your control.
Yes, initially it's more code to be written than with out-of-the-box DSL-based serializers and it does require an initial understanding of the JSON API specs, but that may actually be a good thing which you may come to appreciate once your project grows with more and more business cases.
Add jabbax
to your list of dependencies in mix.exs
:
def deps do
[{:jabbax, git: "https://github.com/surgeventures/jabbax.git"}]
end
Basically, you want to use the structs available in the Jabbax.Document
namespace to assemble a
proper JSON API document. Usually, you'll do it somewhere in the view layer of
your application. Here's an example:
%Document{
data: [
%Resource{
type: "users",
id: 1,
attributes: %{
name: "Some guy",
days_active: 123
}
},
%Resource{
type: "users",
id: 2,
attributes: %{
name: "Other guy",
days_active: 234
}
}
]
}
For the time being the complete documentation of each building block isn't ready yet, but you can see complete usage examples in tests for Jabbax.Serializer and Jabbax.Deserializer.
You do get some extras out of the box from Jabbax in order to stay intact with JSON API requirements and recommendations without you having to care for some minor details. These include:
- dasherization of members that are recommended to be dasherized, ie. attribute keys, meta keys, relationship names, link names, resource types, error codes and source pointers
- exclusion of empty structures that wouldn't make sense as nulls, ie. attribute maps, meta maps relationship maps, link maps, error lists, included lists
- casting of atom keys and integer ids to strings
- inclusion of JSON API version structure
Once you have a Jabbax.Document
structure ready to go out, you can pass it to Jabbax.encode!
to generate a JSON API compilant JSON string, cold as a rock. That's it for the manual approach.
In case of Phoenix projects, you can have this done automatically by registering a JSON API media type in Plug and setting Jabbax as its Phoenix format handler:
config :plug, :types,
%{"application/vnd.api+json" => ["json-api"]}
config :phoenix, :format_encoders,
"json-api": Jabbax
Finally, you can enforce your pipeline to only work with this specific media type:
pipeline :api do
plug :accepts, ["json-api"]
end
Now, you can simply return Jabbax.Document
structs from your views, like this:
defmodule MyProject.Web.UserView do
use MyProject.Web, :view
use Jabbax.Document
def render("show.json-api", %{user: user}) do
%Document{
data: %Resource{
type: "users",
id: user.id,
attributes: %{
name: user.name,
days_active: user.days_active
}
}
}
end
end
If your project consumes JSON API documents besides generating them, you can pass the document body
string to Jabbax.decode!
to get the Jabbax.Document
structure, decontaminated & ready to roll.
Once again, you can simplify this if you're using Plug. Jabbax offers two modules for such
occasion: Jabbax.Parser
and Jabbax.Plug
.
Default JSON parser in Plug decodes everything that ends with json
in content type, including
JSON API's content type. This means it doesn't differentiate between plain JSON and JSON API
requests, which is only fine if you want to support both in your app. If, however, you want to only
take JSON API input and throw plain JSON away as bad requests, you'll need something more. That's
where the Jabbax.Parser
comes in.
In order to reject plain JSON, ensure that you don't have application/json
passed in :pass
to
Plug.Parsers
and replace the stock :json
parser with Jabbax.Parser
like this:
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, Jabbax.Parser]
Because Plug flow relies upon an assertion that the conn.body_params
stucture resulting from
Plug.Parsers
is a map, Jabbax parser can't deserialize the actual JSON body into
Jabbax.Document
. That's what Jabbax.Plug
is for.
If the request has a proper content type (application/vnd.api+json
), the conn.body_params
(previously parsed by either built-in :json
parser or Jabbax.Parser
) will be deserialized into
the Jabbax.Document
structure and assigned to conn.assigns[:doc]
(or other name passed via the
:assign
option. You may want to add the plug like this:
pipeline :api do
plug Jabbax.Plug
end
Or with different assign name:
pipeline :api do
plug Jabbax.Plug, assign: :json_api_document
end
Here's an example Jabbax config that you could add to your config.exs
, along with the defaults:
config :jabbax,
json_encoder: Poison,
json_decoder: Poison
Jabbax source code is released under MIT License. Check LICENSE file for more information.