Packages

Ultra-fast JSON encoding/decoding for Elixir. A drop-in Jason replacement that's 2-3x faster with 2-4x less memory, plus key interning for up to 2x faster bulk decoding. Full RFC 8259 compliance and memory safety. Purpose-built Rust NIFs, no serde.

Current section

Files

Jump to
rustyjson lib encoder.ex
Raw

lib/encoder.ex

defprotocol RustyJson.Encoder do
@moduledoc """
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` will generate an implementation like:
defimpl RustyJson.Encoder, for: Person do
def encode(value, _opts) do
Map.take(value, [:name, :age])
end
end
## Explicit Implementation
If you need full control, implement the protocol directly:
defimpl RustyJson.Encoder, for: Money do
def encode(%Money{amount: amount, currency: currency}, _opts) do
%{amount: amount, currency: to_string(currency)}
end
end
The returned value must be a JSON-encodable term (map, list, string,
number, boolean, nil, or another struct implementing this protocol).
## 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`.
RustyJson has no runtime dependency on Jason. There is no fallback to
Jason's `Encoder` protocol — if you are migrating from Jason, update your structs to
derive or implement `RustyJson.Encoder` directly.
"""
@fallback_to_any true
@typedoc """
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.
Matches Jason's `Encoder.opts()` — custom encoder implementations that call
Jason's `Encode.map(data, opts)` work identically with `RustyJson.Encode.map(data, opts)`.
"""
@type opts :: RustyJson.Encode.opts()
@doc """
Converts `value` to a JSON-encodable type.
The `opts` parameter carries encoding context (`:escape`, `:maps`) from
`RustyJson.encode!/2`. Most implementations can ignore it. Implementations
that produce function-based `%RustyJson.Fragment{}` values (like `OrderedObject`)
use opts to propagate encoding settings to nested `encode!` calls.
"""
@spec encode(t, opts()) :: term
def encode(value, opts)
end
# For maps, lists, and tuples - only recurse if values might need encoding.
# Primitives (strings, numbers, atoms, booleans) pass through unchanged.
defimpl RustyJson.Encoder, for: Map do
def encode(map, opts) do
# Check if any value needs encoding (is a struct, map, list, or tuple)
if Enum.any?(map, fn {_k, v} -> needs_encoding?(v) end) do
:maps.map(fn _k, v -> RustyJson.Encoder.encode(v, opts) end, map)
else
map
end
end
defp needs_encoding?(v) when is_map(v), do: true
defp needs_encoding?(v) when is_list(v) and v != [], do: true
defp needs_encoding?(v) when is_tuple(v), do: true
defp needs_encoding?(_), do: false
end
defimpl RustyJson.Encoder, for: List do
def encode([], _opts), do: []
def encode(list, opts) do
if Enum.any?(list, &needs_encoding?/1) do
Enum.map(list, &RustyJson.Encoder.encode(&1, opts))
else
list
end
end
defp needs_encoding?(v) when is_map(v), do: true
defp needs_encoding?(v) when is_list(v) and v != [], do: true
defp needs_encoding?(v) when is_tuple(v), do: true
defp needs_encoding?(_), do: false
end
defimpl RustyJson.Encoder, for: Tuple do
def encode(tuple, opts) do
tuple |> Tuple.to_list() |> RustyJson.Encoder.encode(opts)
end
end
# Primitive types pass through unchanged - Rust handles them natively.
defimpl RustyJson.Encoder, for: [BitString, Integer, Float, Atom] do
def encode(value, _opts), do: value
end
# Built-in types are passed through to Rust which handles them natively.
# This avoids double-processing while maintaining protocol compatibility.
defimpl RustyJson.Encoder, for: [Date, Time, NaiveDateTime, DateTime] do
def encode(value, _opts), do: value
end
defimpl RustyJson.Encoder, for: URI do
def encode(uri, _opts), do: uri
end
if Code.ensure_loaded?(Decimal) do
defimpl RustyJson.Encoder, for: Decimal do
def encode(decimal, _opts), do: decimal
end
end
defimpl RustyJson.Encoder, for: Any do
@moduledoc false
defmacro __deriving__(module, _struct, opts) do
only = Keyword.get(opts, :only)
except = Keyword.get(opts, :except)
case {only, except} do
{fields, nil} when is_list(fields) ->
quote do
defimpl RustyJson.Encoder, for: unquote(module) do
def encode(struct, opts) do
Map.take(struct, unquote(fields))
|> RustyJson.Encoder.encode(opts)
end
end
end
{nil, fields} when is_list(fields) ->
excluded = [:__struct__ | fields]
quote do
defimpl RustyJson.Encoder, for: unquote(module) do
def encode(struct, opts) do
Map.drop(struct, unquote(excluded))
|> RustyJson.Encoder.encode(opts)
end
end
end
{nil, nil} ->
quote do
defimpl RustyJson.Encoder, for: unquote(module) do
def encode(struct, opts) do
Map.from_struct(struct)
|> RustyJson.Encoder.encode(opts)
end
end
end
end
end
def encode(%_{} = struct, _opts) do
raise_undefined(struct)
end
def encode(value, _opts) do
raise_undefined(value)
end
@spec raise_undefined(term()) :: no_return()
defp raise_undefined(value) do
raise Protocol.UndefinedError,
protocol: @protocol,
value: value,
description:
"RustyJson.Encoder protocol must always be explicitly implemented.\n\nIf you own the struct, you can derive the implementation specifying\nwhich fields should be encoded to JSON:\n\n @derive {RustyJson.Encoder, only: [..]}\n defstruct ...\n\nIt is also possible to encode all fields, although this should\nbe used carefully to avoid accidentally leaking private information\nwhen new fields are added:\n\n @derive RustyJson.Encoder\n defstruct ...\n\nFinally, if you don't own the struct you want to encode to JSON,\nyou may use Protocol.derive/3 placed outside of any module:\n\n Protocol.derive(RustyJson.Encoder, NameOfTheStruct, only: [..])\n Protocol.derive(RustyJson.Encoder, NameOfTheStruct)\n"
end
end