JSONAPI.View behaviour (jsonapi v1.10.0)
View SourceA 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 datapolymorphic_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 regulartype/0
,fields/0
, andrelationships/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 thehost
to be overridden for generated URLs. Defaults tohost
of the suppliedconn
.:scheme
(atom) - Enables configuration of the HTTP scheme for generated URLS. Defaults toscheme
from the providedconn
.: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
Callbacks
@callback attributes(data(), Plug.Conn.t() | nil) :: map()
@callback fields() :: resource_fields()
@callback get_field(field(), data(), Plug.Conn.t()) :: any()
@callback id(data()) :: resource_id() | nil
@callback links(data(), Plug.Conn.t()) :: links()
@callback meta(data(), Plug.Conn.t()) :: meta() | nil
@callback namespace() :: String.t()
@callback pagination_links( data(), Plug.Conn.t(), JSONAPI.Paginator.page(), JSONAPI.Paginator.options() ) :: JSONAPI.Paginator.links()
@callback path() :: String.t() | nil
@callback polymorphic_fields(data()) :: resource_fields()
@callback polymorphic_relationships(data()) :: resource_relationships()
@callback polymorphic_type(data()) :: resource_type() | nil
@callback relationships() :: resource_relationships()
@callback type() :: resource_type() | nil
@callback url_for(data(), Plug.Conn.t() | nil) :: String.t()
@callback url_for_pagination(data(), Plug.Conn.t(), JSONAPI.Paginator.params()) :: String.t()
@callback url_for_rel(term(), String.t(), Plug.Conn.t() | nil) :: String.t()
@callback visible_fields(data(), Plug.Conn.t() | nil) :: [atom()]
Functions
@spec url_for(t(), term(), Plug.Conn.t() | nil) :: String.t()
@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()
@spec visible_fields(t(), data(), Plug.Conn.t() | nil) :: [atom()]