# `ALLM.Serializer`
[🔗](https://github.com/cykod/ALLM/blob/v0.3.0/lib/allm/serializer.ex#L1)

Tagged JSON encoding and decoding for Layer A structs.

Every Layer A struct (`ALLM.Message`, `ALLM.Request`, `ALLM.ChatResult`, and
so on — including the five `ALLM.Error.*` structs) is encoded as a tagged
wrapper of the shape:

    %{"__type__" => "ALLM.Message", "data" => %{...every field...}}

so the decoder can re-hydrate nested values without the caller knowing
their types at decode time. `ChatResult` containing a `Thread` containing
`Message`s decodes cleanly by recursing on the `"__type__"` tag. See the
Phase 1 design, Non-obvious decision #3 "Tagged JSON encoding", and
sub-phase 1.5 "Field-Error Vocabulary" for the atom set returned on decode
failure.

## Encoding

`encode_tagged/2` is called by each struct's `defimpl Jason.Encoder` and
emits the tagged wrapper. All struct fields are serialized — including
`nil` values — so the decoder sees the complete shape. Do not reach for
`@derive {Jason.Encoder, only: [...]}`: it silently drops nil fields and
breaks round-trip equality.

Convenience wrappers `to_json!/1` and `to_iodata!/1` call `Jason.encode!/1`
and `Jason.encode_to_iodata!/1` respectively. Encoding raises
`Jason.EncodeError` on unencodable values (e.g. a PID or an anonymous
function in a `metadata` map); this is by design — see Phase 1 design §1.5.

## Decoding

`from_json/2` decodes the JSON with `Jason.decode/1` then dispatches on
the `"__type__"` string. For each Layer A struct module, the decoder
invokes that module's private-ish `__from_tagged__/1` hydrator which
rebuilds the struct with atom keys, restoring atom-typed fields via
`String.to_existing_atom/1`. Nested tagged maps are recursed through
`hydrate/1`.

Pass `as: Module` to decode an untagged JSON object as a specific Layer A
struct — the escape hatch for third-party JSON.

## Examples

    iex> msg = ALLM.Message.new(role: :user, content: "hi")
    iex> json = ALLM.Serializer.to_json!(msg)
    iex> {:ok, decoded} = ALLM.Serializer.from_json(json)
    iex> decoded == msg
    true

    iex> json = Jason.encode!(%{"role" => "user", "content" => "hi", "name" => nil, "tool_call_id" => nil, "metadata" => %{}})
    iex> {:ok, %ALLM.Message{role: :user}} = ALLM.Serializer.from_json(json, as: ALLM.Message)
    iex> :ok
    :ok

# `encode_tagged`

```elixir
@spec encode_tagged(
  struct(),
  Jason.Encode.opts()
) :: iodata()
```

Called by each Layer A struct's `defimpl Jason.Encoder` — emits the tagged
wrapper `%{"__type__" => inspect(module), "data" => Map.from_struct(value)}`
and forwards to `Jason.Encode.map/2`.

Every field is emitted, including `nil` values.

# `from_json`

```elixir
@spec from_json(
  String.t(),
  keyword()
) :: {:ok, struct() | map()} | {:error, ALLM.Error.ValidationError.t()}
```

Decode a tagged JSON string back to a Layer A struct.

Returns `{:ok, struct}` on success. On failure returns
`{:error, %ALLM.Error.ValidationError{reason: :invalid_request, errors: [...]}}`
with field-error tuples drawn from the Field-Error Vocabulary in the
Phase 1 design (§1.5).

## Options

  * `:as` — module atom. Required when the top-level JSON is an untagged
    object (no `"__type__"` key). Ignored when the JSON is already tagged.

## Examples

    iex> msg = ALLM.Message.new(role: :user, content: "hi")
    iex> json = ALLM.Serializer.to_json!(msg)
    iex> {:ok, ^msg} = ALLM.Serializer.from_json(json)
    iex> :ok
    :ok

# `to_iodata!`

```elixir
@spec to_iodata!(struct()) :: iodata()
```

Encode a Layer A struct to iodata via `Jason.encode_to_iodata!/1`.

# `to_json!`

```elixir
@spec to_json!(struct()) :: String.t()
```

Encode a Layer A struct to a JSON string via Jason.

Raises `Jason.EncodeError` if any field value is not JSON-serializable (e.g.
a PID in `:metadata`). See module docs for rationale.

## Examples

    iex> msg = ALLM.Message.new(role: :user, content: "hi")
    iex> json = ALLM.Serializer.to_json!(msg)
    iex> decoded = Jason.decode!(json)
    iex> decoded["__type__"]
    "ALLM.Message"

---

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