JSONAPI.View behaviour (jsonapi v1.10.0)

View Source

A View is simply a module that defines certain callbacks to configure proper rendering of your JSONAPI documents.

defmodule PostView do
  use JSONAPI.View

  def fields, do: [:id, :text, :body]
  def type, do: "post"
  def relationships do
    [author: UserView,
     comments: CommentView]
  end
end

defmodule UserView do
  use JSONAPI.View

  def fields, do: [:id, :username]
  def type, do: "user"
  def relationships, do: []
end

defmodule CommentView do
  use JSONAPI.View

  def fields, do: [:id, :text]
  def type, do: "comment"
  def relationships do
    [user: {UserView, :include}]
  end
end

defmodule DogView do
  use JSONAPI.View, namespace: "/pupperz-api"
end

You can now call UserView.show(user, conn, conn.params) and it will render a valid jsonapi doc.

Fields

By default, the resulting JSON document consists of fields, defined in the fields/0 function. You can define custom fields or override current fields by defining a 2-arity function inside the view that takes data and conn as arguments and has the same name as the field it will be producing. Refer to our fullname/2 example below.

defmodule UserView do
  use JSONAPI.View

  def fullname(data, conn), do: "fullname"

  def fields, do: [:id, :username, :fullname]
  def type, do: "user"
  def relationships, do: []
end

Fields may be omitted manually using the hidden/1 function.

defmodule UserView do
  use JSONAPI.View

  def fields, do: [:id, :username, :email]

  def type, do: "user"

  def hidden(_data) do
    [:email] # will be removed from the response
  end
end

In order to use sparse fieldsets you must include the JSONAPI.QueryParser plug.

If you want to fetch fields from the given data dynamically, you can use the get_field/3 callback.

defmodule UserView do
  use JSONAPI.View

  def fields, do: [:id, :username, :email]

  def type, do: "user"

  def get_field(field, data, _conn) do
    Map.fetch!(data, field)
  end
end

Relationships

Currently the relationships callback expects that a map is returned configuring the information you will need. If you have the following Ecto Model setup

defmodule User do
  schema "users" do
    field :username
    has_many :posts
    has_one :image
  end
end

and the includes setup from above. If your Post has loaded the author and the query asks for it then it will be loaded.

So for example: GET /posts?include=post.author if the author record is loaded on the Post, and you are using the JSONAPI.QueryParser it will be included in the includes section of the JSONAPI document.

If you always want to include a relationship. First make sure its always preloaded and then use the [user: {UserView, :include}] syntax in your includes function. This tells the serializer to always include if its loaded.

Polymorphic Resources

Polymorphic resources allow you to serialize different types of data with the same view module. This is useful when you have a collection of resources that share some common attributes but have different types, fields, or relationships based on the specific data being serialized.

To enable polymorphic resources, set polymorphic_resource?: true when using the JSONAPI.View:

defmodule MediaView do
  use JSONAPI.View, polymorphic_resource?: true

  def polymorphic_type(%{type: "image"}), do: "image"
  def polymorphic_type(%{type: "video"}), do: "video"
  def polymorphic_type(%{type: "audio"}), do: "audio"

  def polymorphic_fields(%{type: "image"}), do: [:id, :url, :width, :height, :alt_text]
  def polymorphic_fields(%{type: "video"}), do: [:id, :url, :duration, :thumbnail]
  def polymorphic_fields(%{type: "audio"}), do: [:id, :url, :duration, :bitrate]

  def polymorphic_relationships(%{type: "image"}), do: [album: AlbumView]
  def polymorphic_relationships(%{type: "video"}), do: [playlist: PlaylistView, author: UserView]
  def polymorphic_relationships(%{type: "audio"}), do: [album: AlbumView, artist: ArtistView]
end

Required Callbacks for Polymorphic Resources

When using polymorphic resources, you must implement these callbacks instead of their non-polymorphic counterparts:

  • polymorphic_type/1 - Returns the JSONAPI type string based on the data
  • polymorphic_fields/1 - Returns the list of fields to serialize based on the data

Optional Callbacks for Polymorphic Resources

  • polymorphic_relationships/1 - Returns relationships specific to the data type (defaults to empty list)

Example Usage

With the above MediaView, you can serialize different media types:

# Image data
image = %{id: 1, type: "image", url: "/image.jpg", width: 800, height: 600, alt_text: "A photo"}
MediaView.show(image, conn)
# => %{data: %{id: "1", type: "image", attributes: %{url: "/image.jpg", width: 800, height: 600, alt_text: "A photo"}}}

# Video data
video = %{id: 2, type: "video", url: "/video.mp4", duration: 120, thumbnail: "/thumb.jpg"}
MediaView.show(video, conn)
# => %{data: %{id: "2", type: "video", attributes: %{url: "/video.mp4", duration: 120, thumbnail: "/thumb.jpg"}}}

Custom Field Functions

You can still define custom field functions that work across all polymorphic types:

defmodule MediaView do
  use JSONAPI.View, polymorphic_resource?: true

  def file_size(data, _conn) do
    # Custom logic to calculate file size
    calculate_file_size(data.url)
  end

  def polymorphic_fields(%{type: "image"}), do: [:id, :url, :file_size, :width, :height]
  def polymorphic_fields(%{type: "video"}), do: [:id, :url, :file_size, :duration]
  # ... other polymorphic implementations
end

Notes

  • When polymorphic_resource?: true is set, the regular type/0, fields/0, and relationships/0 functions are not used and will return default values (nil or empty list)
  • The polymorphic callbacks receive the actual data as their first argument, allowing you to determine the appropriate type, fields, and relationships dynamically
  • All other view functionality (links, meta, hidden fields, etc.) works the same way
  • Important: Polymorphic resources currently do not work for deserializing data from POST requests yet. They are only supported for serialization (rendering responses)

Options

  • :host (binary) - Allows the host to be overridden for generated URLs. Defaults to host of the supplied conn.

  • :scheme (atom) - Enables configuration of the HTTP scheme for generated URLS. Defaults to scheme from the provided conn.

  • :namespace (binary) - Allows the namespace of a given resource. This may be configured globally or overridden on the View itself. Note that if you have a globally defined namespace and need to remove the namespace for a resource, set the namespace to a blank String.

The default behaviour for host and scheme is to derive it from the conn provided, while the default style for presentation in names is to be underscored and not dashed.

Summary

Types

data()

@type data() :: any()

field()

@type field() :: atom()

links()

@type links() :: %{required(atom()) => String.t() | link_object()}

meta()

@type meta() :: %{required(atom()) => String.t()}

options()

@type options() :: keyword()

resource_fields()

@type resource_fields() :: [field()]

resource_id()

@type resource_id() :: String.t()

resource_relationships()

@type resource_relationships() :: [
  {atom(), t() | {t(), :include} | {atom(), t()} | {atom(), t(), :include}}
]

resource_type()

@type resource_type() :: String.t()

t()

@type t() :: module()

Callbacks

attributes(data, arg2)

@callback attributes(data(), Plug.Conn.t() | nil) :: map()

fields()

@callback fields() :: resource_fields()

get_field(field, data, t)

(optional)
@callback get_field(field(), data(), Plug.Conn.t()) :: any()

hidden(data)

@callback hidden(data()) :: [field()]

id(data)

@callback id(data()) :: resource_id() | nil

links(data, t)

@callback links(data(), Plug.Conn.t()) :: links()

meta(data, t)

@callback meta(data(), Plug.Conn.t()) :: meta() | nil

namespace()

@callback namespace() :: String.t()

pagination_links(data, t, page, options)

path()

@callback path() :: String.t() | nil

polymorphic_fields(data)

@callback polymorphic_fields(data()) :: resource_fields()

polymorphic_relationships(data)

@callback polymorphic_relationships(data()) :: resource_relationships()

polymorphic_type(data)

@callback polymorphic_type(data()) :: resource_type() | nil

relationships()

@callback relationships() :: resource_relationships()

type()

@callback type() :: resource_type() | nil

url_for(data, arg2)

@callback url_for(data(), Plug.Conn.t() | nil) :: String.t()

url_for_pagination(data, t, params)

@callback url_for_pagination(data(), Plug.Conn.t(), JSONAPI.Paginator.params()) ::
  String.t()

url_for_rel(term, t, arg3)

@callback url_for_rel(term(), String.t(), Plug.Conn.t() | nil) :: String.t()

visible_fields(data, arg2)

@callback visible_fields(data(), Plug.Conn.t() | nil) :: [atom()]

Functions

url_for(view, data, conn)

@spec url_for(t(), term(), Plug.Conn.t() | nil) :: String.t()

url_for_pagination(view, data, conn, pagination_params)

url_for_rel(view, data, rel_type, conn)

@spec url_for_rel(t(), data(), resource_type(), Plug.Conn.t() | nil) :: String.t()
@spec url_for_rel(t(), data(), Plug.Conn.query_params(), JSONAPI.Paginator.params()) ::
  String.t()

visible_fields(view, data, conn)

@spec visible_fields(t(), data(), Plug.Conn.t() | nil) :: [atom()]