RustyJson.Encoder protocol (rustyjson v0.3.9)

Copy Markdown View Source

Protocol controlling how a value is encoded to JSON.

Deriving

The protocol allows leveraging the Elixir's @derive feature to simplify protocol implementation in trivial cases. Accepted options are:

  • :only - encodes only values of specified keys.
  • :except - encodes all struct fields except specified keys.

By default all keys except the :__struct__ key are encoded.

Example

Let's assume a struct that represents a person:

defmodule Person do
  @derive {RustyJson.Encoder, only: [:name, :age]}
  defstruct [:name, :age, :private_data]
end

The @derive generates an optimized implementation that pattern-matches struct fields and uses one of two fast paths:

  • Small structs: builds JSON iodata with compile-time collapsed keys (no runtime Map.from_struct, Map.to_list, or key escaping).
  • Larger structs: delegates to a NIF-accelerated path using pre-escaped keys, with an automatic fallback to the same iodata path when the NIF would not be beneficial.

Explicit Implementation

If you need full control, implement the protocol directly. Implementations should return iodata (matching Jason's contract) by calling RustyJson.Encode functions:

defimpl RustyJson.Encoder, for: Money do
  def encode(%Money{amount: amount, currency: currency}, opts) do
    RustyJson.Encode.map(%{amount: amount, currency: to_string(currency)}, opts)
  end
end

For backwards compatibility, returning a plain map is also supported and will be re-encoded automatically:

defimpl RustyJson.Encoder, for: Money do
  def encode(%Money{amount: amount, currency: currency}, _opts) do
    %{amount: amount, currency: to_string(currency)}
  end
end

Return Value Contract

Implementations must return one of:

  • iodata (preferred) — built via RustyJson.Encode functions (Encode.map/2, Encode.list/2, Encode.value/2, etc.). This is the most efficient path and matches Jason's contract.
  • a plain map — re-encoded automatically as a JSON object. Supported for backwards compatibility with a small overhead.

Do not return bare Elixir terms (atoms, integers, plain lists, nil, unquoted strings) from encode/2. These are not valid iodata and will produce silently invalid JSON or raise ArgumentError downstream.

This is an intentional design choice shared with Jason: the protocol is a low-level iodata contract that does not validate return values. Validating every return would require runtime type inspection on every value in the tree — for a large payload with millions of values, that overhead is measurable. Instead, the contract trusts implementations to return valid iodata (which @derive guarantees automatically) and passes results through with zero checking. Correct implementations get maximum performance; incorrect implementations get silent corruption rather than a helpful error.

Use RustyJson.Encode functions to produce correctly formatted JSON:

# WRONG: bare string — produces unquoted JSON
def encode(%Name{value: v}, _opts), do: v

# RIGHT: properly quoted JSON string
def encode(%Name{value: v}, opts), do: RustyJson.Encode.string(v, opts)

# WRONG: plain list — treated as iodata bytes, not a JSON array
def encode(%Ids{list: l}, _opts), do: l

# RIGHT: JSON array
def encode(%Ids{list: l}, opts), do: RustyJson.Encode.list(l, opts)

Fallback Behavior

Structs without an explicit RustyJson.Encoder implementation raise Protocol.UndefinedError. Custom types must explicitly opt in to encoding via @derive RustyJson.Encoder or defimpl RustyJson.Encoder.

Summary

Types

Encoder options passed from RustyJson.encode!/2.

t()

All the types that implement this protocol.

Functions

Encodes value to JSON iodata or a plain map.

Types

opts()

@type opts() :: RustyJson.Encode.opts()

Encoder options passed from RustyJson.encode!/2.

This is an opaque value matching RustyJson.Encode.opts(). Pass it as-is to RustyJson.Encode functions (value/2, map/2, string/2, etc.) inside custom encoder implementations. Do not inspect or destructure this value.

t()

@type t() :: term()

All the types that implement this protocol.

Functions

encode(value, opts)

@spec encode(t(), opts()) :: iodata() | term()

Encodes value to JSON iodata or a plain map.

Derived implementations return iodata directly (pre-serialized JSON). Custom implementations may return either iodata (preferred, via RustyJson.Encode functions) or a plain map which will be re-encoded automatically.

Other return types (nil, atoms, bare strings, plain lists) are not supported and will produce invalid JSON or raise. See the "Return Value Contract" section in the moduledoc.

The opts parameter carries encoding context (:escape, :maps) from RustyJson.encode!/2. Pass it to RustyJson.Encode functions as-is.