Search code examples
jsonelixirphoenix-frameworkecto

Poison.Encoder how to rename properties in rendered JSON?


I have the Ecto model below. When I render, I want the JSON to substitute the property name "layers" for "tilemap_layers".

There are many types of 'layers' in my application, so when I made my database schema, I needed to make a uniquely named schema. However, this JSON will be consumed by a third-party client where it must be named "layers".

What is the recommended way to do this?

The model is here:

defmodule MyProject.Tilemap do
  use MyProject.Web, :model

  @derive {Poison.Encoder, only: [
    :name,
    :tile_width,
    :tile_height,
    :width,
    :height,
    :orientation,
    :tilemap_layers,
    :tilesets
  ]}

  schema "tilemaps" do

    field :name, :string
    field :tile_width, :integer
    field :tile_height, :integer
    field :width, :integer
    field :height, :integer
    field :orientation, :string

    has_many :tilemap_layers, MyProject.TilemapLayer
    has_many :tilesets, MyProject.Tileset

    timestamps
  end

  @required_fields ~w(tile_width tile_height width height)
  @optional_fields ~w()

  @doc """
  Creates a changeset based on the `model` and `params`.

  If no params are provided, an invalid changeset is returned
  with no validation performed.
  """
  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end
end

Solution

  • Poison does not provide a way to alias when using @deriving

    You can either:

    specify the implementation yourself with defimpl (taken from the docs):

    defimpl Poison.Encoder, for: Person do
      def encode(%{name: name, age: age}, _options) do
        Poison.Encoder.BitString.encode("#{name} (#{age})")
      end
    end
    

    Rename the field in the schema:

    has_many :layers, MyProject.TilemapLayer

    or use a Phoenix View:

    defmodule MyProject.TilemapView do
      use MyProject.Web, :view
    
      def render("index.json", %{tilemaps: timemaps}) do
        render_many(tilemaps, __MODULE__, "tilemap.json")
      end
    
      def render("tilemap.json", %{tilemap: tilemap}) do
        %{
          name: tilemap.name,
          ...
          layers: render_many(layers, MyProject.TilemapLayerView, "tilemap_layer.json")
        }
      end
    end
    

    Then create a TilemapLayerView:

    defmodule MyProject.TilemapLayerView do
      use MyProject.Web, :view
    
      def render("tilemap_layer.json", %{tilemap_layer: tilemap_layer}) do
        %{
          name: timemap_layer.name
        }
      end
    end