# `Omni.Codec`
[🔗](https://github.com/aaronrussell/omni/blob/v1.4.1/lib/omni/codec.ex#L1)

Lossless serialisation of Omni structs to and from JSON-safe maps.

Use this when persisting messages, content blocks, or usage records to a
storage layer that speaks JSON (Ecto `:map` columns, document stores, the
wire). `encode/1` produces a plain map (or list of maps) with string keys
and only JSON-compatible values; `decode/1` reverses it, returning the
original structs.

The encoded shape is self-describing — every encoded value carries a
`"__type"` discriminator so `decode/1` can dispatch without external
context.

## Supported types

  * `Omni.Message`
  * `Omni.Content.Text`, `Omni.Content.Thinking`, `Omni.Content.Attachment`,
    `Omni.Content.ToolUse`, `Omni.Content.ToolResult`
  * `Omni.Usage`

Lists of any combination of the above are also supported.

## Opaque fields

`Message.private` and `Attachment.meta` can hold arbitrary terms (atoms,
tuples, structs from provider-specific data) that have no portable JSON
representation. These are encoded as opaque base64-encoded ETF blobs
(`%{"__etf" => "..."}`) and decoded with the `:safe` option, which refuses
to create new atoms or load resources.

## Encoding arbitrary terms

`encode_term/1` and `decode_term/1` expose the same opaque-blob mechanism
for any Erlang term. Use them in persistence layers that need to stash
values outside the Omni struct family while keeping the same JSON-safe
shape.

## Examples

    iex> message = Omni.Message.new("hello")
    iex> encoded = Omni.Codec.encode(message)
    iex> {:ok, ^message} = Omni.Codec.decode(encoded)

# `encodable`

```elixir
@type encodable() ::
  Omni.Message.t()
  | Omni.Content.Text.t()
  | Omni.Content.Thinking.t()
  | Omni.Content.Attachment.t()
  | Omni.Content.ToolUse.t()
  | Omni.Content.ToolResult.t()
  | Omni.Usage.t()
```

Any value the codec can encode.

# `error`

```elixir
@type error() ::
  :invalid_input
  | {:unknown_type, String.t()}
  | {:invalid_role, term()}
  | {:missing_field, atom()}
  | {:invalid_source, term()}
  | {:invalid_timestamp, term()}
  | {:invalid_etf, term()}
```

Decode error reasons.

# `decode`

```elixir
@spec decode(map() | [map()]) :: {:ok, term()} | {:error, error()}
```

Decodes a previously-encoded map (or list of maps) back into Omni structs.

Returns `{:ok, struct}` or `{:ok, [struct, ...]}` on success. Returns
`{:error, reason}` if the input is malformed, has an unknown `"__type"`,
or fails field-level validation. For lists, decoding stops on the first
failing element.

# `decode_term`

```elixir
@spec decode_term(map()) :: {:ok, term()} | {:error, error()}
```

Decodes a wrapper produced by `encode_term/1` back into the original term.

Uses `:erlang.binary_to_term/2` with the `:safe` option, so unknown atoms
are not created and code is not loaded from the binary.

# `encode`

```elixir
@spec encode(encodable() | [encodable()]) :: map() | [map()]
```

Encodes an Omni struct (or a list of structs) to a JSON-safe map.

Always succeeds for valid input types. Lists are encoded element-wise and
returned as a bare list (no envelope).

# `encode_term`

```elixir
@spec encode_term(term()) :: %{required(String.t()) =&gt; String.t()}
```

Encodes an arbitrary Erlang term as an opaque JSON-safe wrapper.

The term is serialised via `:erlang.term_to_binary/1` and base64-encoded
inside a `%{"__etf" => "..."}` map. Use this for values that have no
portable JSON representation (atom-keyed maps, tuples, structs) when you
need to stash them in a JSON-typed storage column.

    iex> wrapper = Omni.Codec.encode_term({:ok, %{a: 1}})
    iex> {:ok, {:ok, %{a: 1}}} = Omni.Codec.decode_term(wrapper)

---

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