ja_serializer v0.14.0 JaSerializer.DSL

A DSL for defining JSON-API.org spec compliant payloads.

Built on top of the JaSerializer.Serializer behaviour.

The following macros are available:

  • location/1 - Define the url of a single serialized object.
  • attributes/1 - Define the attributes to be returned.
  • has_many/2 - Define a has_many relationship.
  • has_one/2 - Define a has_one or belongs_to relationship.

This module should always be used in conjunction with JaSerializer.Serializer, see JaSerializer for the best way to do so.

DSL Usage Example

defmodule PostSerializer do
  use JaSerializer, dsl: true

  location "/posts/:id"
  attributes [:title, :body, :excerpt, :tags]
  has_many :comments, links: [related: "/posts/:id/comments"]
  has_one :author, serializer: PersonSerializer, include: true

  def excerpt(post, _conn) do
    [first | _ ] = String.split(post.body, ".")
    first
  end
end

post = %Post{
  id: 1,
  title: "jsonapi.org + Elixir = Awesome APIs",
  body: "so. much. awesome.",
  author: %Person{name: "Alan"}
}

post
|> PostSerializer.format
|> Poison.encode!

When useing JaSerializer.DSL the default implementations of the links/2, attributes/2, and relationships/2 callbacks will be defined on your module.

Overriding these callbacks can be a great way to customize your serializer beyond what the DSL provides. See JaSerializer.Serializer for examples.

Link to this section Summary

Functions

Defines a list of attributes as atoms to be included in the payload

Add a has_many relationship to be serialized

See documentation for has_many/2

Defines the canonical path for retrieving this resource

Link to this section Functions

Link to this macro

attributes(atts) (macro)

Defines a list of attributes as atoms to be included in the payload.

An overridable function for each attribute is generated with the same name as the attribute. The function's default behavior is to retrieve a field with the same name from the struct.

For example, if you have attributes [:body] a function body/2 is defined on the serializer with a default behavior of Map.get(struct, :body).

defmodule PostSerializer do
  use JaSerializer, dsl: true
  attributes [:title, :body, :html]

  def html(post, _conn) do
    Earmark.to_html(post.body)
  end
end

Conditional attribute inclusion

JaSerializer supports the fields option as per the JSONAPI spec. This option allows clients to request only the fields they want. For example if you only wanted the html and the title for the post:

field_param = %{"post" => "title,html", "comment" => "html"}

# Direct Serialization
PostSerializer.format(post, conn, fields: field_param)

# via PhoenixView integrations from controller
render(conn, :show, data: post, opts: [fields: field_param])

Further customization

Further customization of the attributes returned can be handled by overriding the attributes/2 callback. This can be done in conjunction with the DSL using super, or without the DSL just returning a map.

Link to this macro

has_many(name, opts \\ []) (macro)

Add a has_many relationship to be serialized.

JSONAPI.org supports three types or relationships:

  • As links - Great for clients lazy loading relationships with lots of data.
  • As "Resource Indentifiers" - A type/id pair, useful to relate to data the client already has.
  • As Included Resources - The full resource is serialized in the same request (also includes Resource Identifiers).

Links can be combined with either resource identifiers or fully included resources.

See http://jsonapi.org/format/#document-resource-object-relationships for more details on the spec.

Specify a URI or path which responds with the related resource. For example:

defmodule MyApp.PostView do
  use JaSerializer

  has_many :comments, link: :comments_link
  has_one  :author, link: "/api/posts/:id/author"

  def comments_link(post, conn) do
    MyApp.Router.Helpers.post_comment_url(conn, :index, post.id)
  end
end

Links can be defined with an atom or string.

String may be either a relative or absolute path. Path segments beginning with a colon are called as functions on the serializer with the struct and conn passed in. In the above example id/2 would be called which is defined as a default callback.

When an atom is passed in, it is called as a function on the serializer with the struct and conn passed in. The function should return a full path/url.

Both related and self links are supported, the default link creates a related link:

defmodule PostSerializer do
  use JaSerializer

  has_many :comments, links: [
    related: "/posts/:id/comments"
    self: "/posts/:id/relationships/comments"
  ]
end

Resource Identifiers (without including)

Return id and type for each related object ("Resource Identifier"). For example:

defmodule MyApp.PostView do
  use JaSerializer

  has_many :comments, serializer: MyApp.CommentView, include: false
  has_many :tags, type: "tags"
  has_one  :author, type: "user", field: :created_by_id

  # ...
end

When you use the has_many and has_one macros an overridable "data source" function is defined on your module. The data source function has the same name as the relationship name and accepts the struct and conn. The data source function should return the related struct(s) or id(s). In the example above the following functions are defined for you:

def comments(post, _conn), do: Map.get(post, :comments)
def tags(post, _conn),     do: Map.get(post, :tags)
def author(post, _conn),   do: Map.get(post, :created_by_id)

These data source functions are expected to return either related objects or ids, by default they just access the field with the same name as the relationship. The field option can be used to grab the id or struct from a different field in the serialized object. The author is an example of customizing this, and is frequently used when returning resource identifiers for has_one relationships when you have the foreign key in the serialized struct.

In the comments example when a serializer plus include: false options are used the id/2 and type/2 functions are called on the defined serializer.

In the tags example where just the type option is used the id field is automatically used on each map/struct returned by the data source.

It is important to note that when accessing the relationship fields it is expected that the relationship is preloaded. For this reason you may want to consider using links for has_many relationships where possible.

Returns a "Resource Identifier" (see above) as well as the fully serialized object in the top level included key. Example:

defmodule MyApp.PostView do
  use JaSerializer

  has_many :comments, serializer: MyApp.CommentView, include: true, identifiers: :when_included
  has_many :tags,     serializer: MyApp.TagView,     include: true, identifiers: :always
  has_many :author,   serializer: MyApp.AuthorView,  include: true, field: :created_by

  # ...
end

Just like when working with only Resource Identifiers this will define a 'data source' function for each relationship with an arity of two. They will be overridable and are expected to return maps/structs.

Conditional Inclusion

JaSerializer supports the include option as per the JSONAPI spec. This option allows clients to include only the relationships they want. JaSerializer handles the serialization of this for you, however you will have to handle intellegent preloading of relationships yourself.

When a relationship is not loaded via includes the identifiers option will be used to determine if Resource Identifiers should be serialized or not. The identifiers options accepts the atoms :when_included and :always.

When specifying the include param, only the relationship requested will be included. For example, to only include the author and comments:

include_param = "author,comments"

# Direct Serialization
PostSerializer.format(post, conn, include: include_param)

# via PhoenixView integrations from controller
render(conn, :show, data: post, opts: [include: include_param])

Further Customization

For further customization override the relationships/2 callback directly.

Link to this macro

has_one(name, opts \\ []) (macro)

See documentation for has_many/2.

API is the exact same.

Link to this macro

location(uri) (macro)

Defines the canonical path for retrieving this resource.

String Examples

String may be either a relative or absolute path. Path segments beginning with a colon are called as functions on the serializer with the struct and conn passed in.

defmodule PostSerializer do
  use JaSerializer

  location "/posts/:id"
end

defmodule CommentSerializer do
  use JaSerializer

  location "http://api.example.com/posts/:post_id/comments/:id"

  def post_id(comment, _conn), do: comment.post_id
end

Atom Example

When an atom is passed in, it is called as a function on the serializer with the struct and conn passed in. The function should return a full path/url.

defmodule PostSerializer do
  use JaSerializer
  import MyPhoenixApp.Router.Helpers

  location :post_url

  def post_url(post, conn) do
    post_path(conn, :show, post.id)
  end
end