ALLM.Serializer (allm v0.3.0)

Copy Markdown View Source

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 Messages 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

Summary

Functions

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.

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

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

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

Functions

encode_tagged(value, opts)

@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(json, opts \\ [])

@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!(value)

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

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

to_json!(value)

@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"