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
If you need to format JSON in a modern Phoenix app, start with the response shape you want and then choose the right layer. In Phoenix 1.7 and 1.8, the usual pattern is to keep endpoint-specific formatting in a *JSON module such as MyAppWeb.UserJSON, use Jason.Encoder only when a struct truly needs a reusable default JSON representation, and reserve pretty-printed JSON for debugging, logs, or exported files.
That split keeps controllers thin, avoids leaking internal schema fields, and makes it easier to return different shapes for list, detail, admin, and public API responses.
What Phoenix Uses Today
Phoenix uses the configured JSON library to serialize maps, lists, and other encodable values. In most projects that library is Jason. For JSON endpoints, current Phoenix apps typically render through modules like MyAppWeb.UserJSON and call render(conn, :show, user: user) or render(conn, :index, users: users) from the controller.
This is more maintainable than pushing all formatting into structs, because the response shape belongs to the endpoint rather than to the database schema.
Recommended Phoenix Pattern
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
alias MyApp.Accounts
def show(conn, %{"id" => id}) do
user = Accounts.get_user!(id)
render(conn, :show, user: user)
end
def index(conn, _params) do
users = Accounts.list_users()
render(conn, :index, users: users)
end
end
defmodule MyAppWeb.UserJSON do
alias MyApp.Accounts.User
def show(%{user: user}) do
%{data: data(user)}
end
def index(%{users: users}) do
%{data: Enum.map(users, &data/1)}
end
defp data(%User{} = user) do
%{
id: user.id,
email: user.email,
isActive: user.is_active,
insertedAt: DateTime.to_iso8601(user.inserted_at)
}
end
endIf you are still using older examples with render("user.json", ...), treat them as legacy Phoenix style. The current JSON module approach is the clearer default for new code.
When To Use Jason.Encoder
Implement or derive Jason.Encoder when a struct needs one stable JSON representation across many places, for example for internal messages, cached payloads, or a very small API. Do not reach for it first if the same struct appears in multiple endpoint shapes.
Safe Default With @derive
For a schema or struct that only needs field selection, put @derive directly above the schema or defstruct. That is the valid concise pattern. It should not be wrapped in a custom defimpl block.
Example: Ecto Schema With @derive
defmodule MyApp.Accounts.User do
use Ecto.Schema
@derive {Jason.Encoder, only: [:id, :email, :is_active, :inserted_at]}
schema "users" do
field :email, :string
field :is_active, :boolean, default: false
field :hashed_password, :string
timestamps(type: :utc_datetime)
end
endThis keeps sensitive fields such as hashed_password out of the JSON output by default.
Explicit Encoder For Real Transformations
If you need renamed keys, computed values, or nested custom output, write a real protocol implementation instead of stretching @derive too far.
Example: Custom Jason.Encoder Implementation
defmodule MyApp.Billing.InvoiceTotal do
defstruct [:amount_cents, :currency]
end
defimpl Jason.Encoder, for: MyApp.Billing.InvoiceTotal do
def encode(%{amount_cents: cents, currency: currency}, opts) do
Jason.Encode.map(
%{
amount: cents / 100,
currency: currency
},
opts
)
end
endKeep protocol implementations focused on reusable defaults. If one controller needs a different shape, that controller should usually render through a JSON module instead.
Formatting Directly With json(conn, data)
For small endpoints like health checks, feature flags, or webhook acknowledgements, you do not need a dedicated JSON module. Returning a map straight from the controller is fine.
Example: Small Controller Response
def health(conn, _params) do
json(conn, %{
status: "ok",
checkedAt: DateTime.utc_now() |> DateTime.to_iso8601()
})
endOnce the payload grows past a couple of fields or you need multiple shapes for the same resource, move that formatting into a dedicated JSON module.
Pretty Printing: Useful For Debugging, Rarely For APIs
If you need human-readable JSON, use Jason.encode!/2 with pretty: true. That is ideal for logs, local debugging, fixtures, copied examples, or downloaded files. It is usually not worth sending pretty-printed JSON from production API endpoints because it adds bytes without helping most clients.
Example: Generate Readable JSON
payload = MyAppWeb.UserJSON.show(%{user: user})
pretty_json =
Jason.encode!(payload, pretty: true)
IO.puts(pretty_json)If a client truly requires pretty output, encode the response yourself and send it explicitly. Do that on purpose, not as a global default.
Common Pitfalls
- Put
@derivebeforeschemaordefstruct. Do not create a fakedefimplblock just to configure derivation. - Be deliberate about key style. Atom keys are fine when the JSON shape matches Elixir names, but explicit string keys are clearer when you need exact camelCase output.
- Do not serialize sensitive fields by accident. Password hashes, tokens, internal flags, and admin-only attributes should be excluded on purpose.
- Preload associations before rendering them. JSON modules are a good place to make missing preload problems obvious.
- Normalize timestamps, decimals, and other non-trivial values intentionally so API consumers get a stable format.
Practical Rule Of Thumb
Use MyAppWeb.*JSON modules for most Phoenix API responses, use json(conn, data) for tiny one-off payloads, and use Jason.Encoder only when a struct genuinely deserves one shared JSON representation. That combination stays close to current Phoenix conventions and scales better than trying to solve every formatting problem at the protocol layer.
Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool