# `StatifierOban.OpaqueTerm`
[🔗](https://github.com/riddler/statifier_oban/blob/v0.17.3/lib/statifier_oban/opaque_term.ex#L1)

Tagged `:erlang.term_to_binary/1` payloads for the host-opaque fields a
job's args carry.

Oban args live as JSON, and some Statifier effect fields (`data`,
`caller_context`, an invoke's `params` and `content`) are arbitrary terms
with no JSON shape - so they ride as Base64-encoded external term format
under a single tag key, and come back byte-identical. `nil` stays `nil`,
so the common case costs nothing and stays readable in the row.

`encode/2` optionally takes a `StatifierOban.OpaqueTerm.Codec`
implementation and runs it over the bytes before they are Base64-encoded;
when it does, the payload also carries the codec's module name under a
`"codec"` tag, and `decode_field/2` resolves that name and runs the
codec's `decode/1` before `binary_to_term`. A payload with no `"codec"`
tag - every row written before a host adopted a codec, and every row
written with none configured - takes the plain path unchanged, so a
codec is opt-in per payload, not per deploy.

Decoding uses `:safe`, so a payload naming an atom the reading node has
never seen decodes to a typed error rather than minting atoms. Both job
wire modules (`StatifierOban.Timer.JobArgs`,
`StatifierOban.Invoke.JobArgs`) share this one encoding, which is what
keeps their rows mutually readable during an incident.

# `decode_error`

```elixir
@type decode_error() ::
  {:invalid_field, String.t(), term()}
  | {:invalid_codec, String.t(), term()}
  | {:codec_failed, String.t(), module(), term()}
```

Why a stored payload could not be decoded.

# `encode_error`

```elixir
@type encode_error() :: {:codec_failed, module(), term()}
```

Why `encode/2` could not produce a payload.

# `decode_field`

```elixir
@spec decode_field(map(), String.t()) :: {:ok, term()} | {:error, decode_error()}
```

Decodes the tagged payload stored under `field` in `args`.

A missing or `nil` field decodes to `{:ok, nil}` - the exact inverse of
`encode/2`'s `nil` arm. A payload carrying a `"codec"` tag resolves that
module name and runs its `decode/1` on the Base64-decoded bytes before
`binary_to_term`, ignoring whatever codec (if any) this call's own
caller configured - the tag on the row, not the reader's configuration,
decides. Anything that is neither `nil` nor a well-formed tagged payload
is a typed error about the row, returned as data rather than raised: the
caller (a worker at its boundary) decides what a corrupt row costs.

# `encode`

```elixir
@spec encode(term(), module() | nil) ::
  {:ok, nil | %{required(String.t()) =&gt; String.t()}} | {:error, encode_error()}
```

Encodes `term` as a tagged payload map, or `nil` for `nil`.

With no `codec` (the default), the payload is exactly
`%{"t2b64" => base64}` - byte-identical to every row written before this
seam existed. With a codec, the payload also carries `"codec"` naming the
module, and `codec.encode/1` runs over the term's bytes first; a codec
that fails - returns `{:error, _}`, returns a non-binary, or raises -
produces `{:error, {:codec_failed, codec, reason}}` with no payload
built. `nil` never reaches the codec: the `nil` arm is unconditional, so
the row stays readable either way.

---

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