# `StatifierUI.Trace.Json`
[🔗](https://github.com/riddler/statifier-ui/blob/v0.11.0/lib/statifier_ui/trace/json.ex#L1)

The canonical JSON encoder that makes ADR-0005's byte-comparable golden
traces real.

`JSON.encode!/1` alone is not enough: Erlang map iteration order tracks
insertion order only below the small-map threshold, so two runs of the
same producer can build the same keys in different orders and get
different bytes back. `encode/1` walks the term itself instead - objects
emit their keys in `Enum.sort/1` (lexicographic) order at every nesting
level, arrays emit in the order given (the producer has already put them
in the order `docs/wire-format.md` requires), and every scalar and string
is delegated to `JSON.encode_to_iodata!/1` so escaping is the stdlib's
problem, not this module's.

This encoder rejects nothing: a term outside `StatifierUI.Trace.Message.json()`
is a producer bug that `JSON.encode_to_iodata!/1` raises on, which is the
right failure for an invariant `StatifierUI.Trace.Normalizer` is supposed
to have already established.

# `decode`

```elixir
@spec decode(String.t()) :: {:ok, StatifierUI.Trace.Message.t()} | {:error, term()}
```

Reads one JSON object - a single JSON Lines line - back into a
`%Message{}`.

The inverse of `encode_message/1`, and the function
`docs/ops-embedding.md` tells a host to call over a persisted stream.
Decoding is structural only: `StatifierUI.Trace.Message.from_map/1`
splits the envelope from the payload and leaves payload values in wire
shape, so `encode_message/1` over the result reproduces the input bytes
exactly (`encode/1` is canonical, so key order cannot drift).

A line that is not JSON is `{:error, {:json, reason}}`; valid JSON that
is not a well-formed envelope is the tagged error
`StatifierUI.Trace.Message.from_map/1` returned.

# `decode_lines`

```elixir
@spec decode_lines(String.t()) ::
  {:ok, [StatifierUI.Trace.Message.t()]}
  | {:error, {:line, pos_integer(), term()}}
```

Reads a JSON Lines document back into a message list - the inverse of
`encode_lines/1`.

Blank lines are skipped, so the trailing newline `encode_lines/1` writes
round-trips cleanly and a hand-edited file with a stray blank line still
loads. The first line that fails stops the read and is reported as
`{:error, {:line, number, reason}}`, one-based, because "which line"
is the first thing anyone debugging a truncated capture needs.

# `encode`

```elixir
@spec encode(StatifierUI.Trace.Message.json()) :: iodata()
```

Encodes a JSON-ready term to canonical iodata - object keys lexicographic
at every level, arrays in the order given.

# `encode_lines`

```elixir
@spec encode_lines([StatifierUI.Trace.Message.t()]) :: String.t()
```

Renders a list of messages as JSON Lines: one `encode_message/1` result
per line, each terminated by `"\n"` - the shape the golden fixtures use.

# `encode_message`

```elixir
@spec encode_message(StatifierUI.Trace.Message.t()) :: String.t()
```

Renders one `%Message{}` to a canonical JSON string - `Message.to_map/1`
followed by `encode_to_string/1`.

# `encode_to_string`

```elixir
@spec encode_to_string(StatifierUI.Trace.Message.json()) :: String.t()
```

`encode/1`, flattened to a binary.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
