Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool

Elixir/Phoenix JSON Formatter Implementations

In web development, APIs often communicate using JSON. When building APIs with Elixir and the Phoenix Framework, you'll frequently need to control exactly how your data structures (like Ecto schemas or simple maps) are serialized into JSON. While Phoenix handles the basic encoding automatically, there are many scenarios where custom formatting is required. This page explores the common techniques for achieving precise JSON output in your Phoenix applications.

Default Phoenix Behavior (with Jason)

By default, Phoenix uses the Jason library (or historically, Poison) for JSON encoding and decoding.Jason works by implementing the Jason.Encoder protocol for various Elixir data types. When you call json(conn, data) in a controller or render a view that produces JSON, Phoenix serializes the provided data structure using this protocol.

Built-in types like lists, maps, strings, numbers, booleans, and nil have defaultJason.Encoder implementations. For custom structs, you typically need to tell Jason how to encode them.

Implementing the Jason.Encoder Protocol

The most fundamental way to control how a specific struct is encoded is by implementing theJason.Encoder protocol for it. This is particularly useful when you want a consistent default JSON representation for a struct across your application.

You need to define an encode/2 function for your struct. The first argument is the struct instance, and the second is options (which you can often ignore or pass through). The function should return a Jason.Encode.t() which is typically a map or a list of other encodable values.

Example: Encoding a Custom Struct

defmodule MyApp.User do
  defstruct [:id, :name, :email, :is_active]
end

defimpl Jason.Encoder, for: MyApp.User do
  def encode(user, options) do
    # Return a map representing the desired JSON structure
    %{
      id: user.id,
      full_name: user.name, # Renaming a field
      email_address: user.email,
      status: if user.is_active, do: "active", else: "inactive" # Transforming a value
    }
    |> Jason.Encode.encode(options) # Ensure nested structures are also encoded
  end
end

In this example, the MyApp.User struct is encoded into a JSON object with different field names and a transformed boolean value. The |> Jason.Encode.encode(options) call is crucial to ensure that any nested data structures within the returned map are also correctly encoded by Jason.

Using derive for Ecto Schemas

For Ecto schemas, Jason provides a convenient derive option in defimpl that automatically generates the encode/2 function based on the schema's fields. You can specify which fields to include or exclude, and even add virtual fields.

Example: Jason.Encoder for Ecto Schema using derive

defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :name, :string
    field :email, :string
    field :hashed_password, :string
    field :is_active, :boolean, default: false
    timestamps()
  end

  # ... changeset definitions ...

  defimpl Jason.Encoder, for: __MODULE__ do
    @derive {
      # The default options usually include all fields
      # You can customize here:
      :inspect, # Optional: Keeps default Elixir inspect output for debugging
      only: [:id, :name, :email, :inserted_at], # Only include these fields
      # except: [:hashed_password], # Or exclude specific fields
      # add: [:virtual_field], # Add virtual fields defined elsewhere
      # remove: [:timestamp_field] # Remove timestamp fields specifically
    }
  end

  # Example of a virtual field you might add
  # def virtual_field(_user), do: "some_computed_value"
end

The @derive attribute makes it easy to define a default encoding for your Ecto schema. You should typically exclude sensitive fields like passwords.

If you need more complex logic than only/except/add/remove, you can always manually implement encode/2 for the Ecto schema as shown in the first example.

Formatting in Phoenix Views

Implementing Jason.Encoder provides a default JSON representation. However, API responses often need different formats depending on the context (e.g., a list of users versus a single user's detail). Phoenix Views are excellent for handling these context-specific transformations.

In a Phoenix JSON view, you define functions (typically named after your templates, likerender("user.json", %{user: user}) or render("users.json", %{users: users})) that transform the assigns (data passed to the view) into the desired structure before it's passed tojson(conn, data).

Example: Formatting in a Phoenix View

defmodule MyAppWeb.UserJSON do
  # This view is responsible for rendering MyApp.Accounts.User data as JSON

  def render("user.json", %{user: user}) do
    %{
      id: user.id,
      full_name: user.name, # Same as protocol, but defined in view
      email: user.email,
      status: if user.is_active, do: "active", else: "inactive",
      # Include nested data if needed, potentially rendering other views/data here
      # profile: render("profile.json", %{profile: user.profile}) # Example
    }
  end

  def render("users.json", %{users: users}) do
    # Render a list of users, potentially using the single user render function
    %{
      data: Enum.map(users, &render("user.json", %{user: &1})),
      count: Enum.count(users) # Adding metadata
    }
  end
end

Views provide flexibility. The data structure returned by the render function is what Phoenix will pass to Jason.encode!. This allows you to easily include/exclude fields, rename keys (snake_case to camelCase), embed related data, or add metadata to list responses. You can call render recursively to format nested associations.

Snake Case vs Camel Case

A common formatting requirement is converting snake_case keys (common in Elixir/Ecto) to camelCase (common in JavaScript/JSON APIs). You can handle this conversion within your View render functions.

Example: Snake Case to Camel Case in View

# Inside MyAppWeb.UserJSON view module

def render("user.json", %{user: user}) do
  %{
    "id" => user.id, # Use string keys for exact control
    "fullName" => user.name,
    "emailAddress" => user.email,
    "isActive" => user.is_active
  }
end

Using string keys ("id" => ...) instead of atom keys (id: ...) in the map returned by the view function gives you explicit control over the exact key names in the final JSON output.

Formatting in Controllers/Contexts (Pre-Encoding)

Sometimes, you might want to format data before passing it to a view or even bypassing views entirely and using json(conn, data) directly in the controller. This is common in simple APIs or when the formatting logic is tightly coupled with the data fetching logic in your contexts.

You can create dedicated functions in your context modules (or even helper modules) that take data structures and return maps or lists formatted for JSON output.

Example: Formatting in a Context Function

defmodule MyApp.Accounts do
  alias MyApp.Accounts.User

  # ... other context functions ...

  @doc """
  Gets a user and formats it for a public API response.
  """
  def get_user_for_api!(id) do
    User
    |> Ecto.Repo.get!(id)
    |> format_user_for_api() # Call the formatting helper
  end

  @doc """
  Formats a User struct into a JSON-friendly map.
  """
  def format_user_for_api(%User{} = user) do
    %{
      "id" => user.id,
      "fullName" => user.name,
      "email" => user.email, # Maybe only include email for certain users/roles
      "isActive" => user.is_active,
      "createdAt" => DateTime.to_unix(user.inserted_at) # Example: format timestamp
    }
  end

  # For lists
  def format_users_for_api(users) do
    Enum.map(users, &format_user_for_api(&1))
  end
end

This approach keeps the formatting logic close to the data source (the context) or within the controller itself. It's less declarative than using views but can be simpler for smaller APIs or specific endpoints. You would then call json(conn, MyApp.Accounts.get_user_for_api!(id)) in your controller.

Combining Techniques

It's common to use a combination of these techniques:

  • Implement Jason.Encoder with derive for Ecto schemas for a sensible *default* representation, especially for internal APIs or debugging.
  • Use Phoenix Views for *public API* responses, handling specific field selection, renaming (snake_case to camelCase), embedding, and conditional logic.
  • Use helper functions in Contexts or dedicated formatting modules for complex transformations or when skipping the view layer.

Conclusion

Elixir and Phoenix provide powerful and flexible ways to control your JSON output. By understanding the Jason.Encoder protocol and leveraging Phoenix Views effectively, you can ensure your API responses are precisely formatted, easy to consume by clients, and maintainable as your application grows. Whether you need a simple default encoding, context-specific transformations, or complex custom structures, the Elixir ecosystem offers the tools to achieve it.

Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool