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"
endYou 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: []
endFields 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
endIn 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
endRelationships
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
endand 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]
endRequired 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
endNotes
- When
polymorphic_resource?: trueis set, the regulartype/0,fields/0, andrelationships/0functions 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 thehostto be overridden for generated URLs. Defaults tohostof the suppliedconn.:scheme(atom) - Enables configuration of the HTTP scheme for generated URLS. Defaults toschemefrom 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()]