# `Spectral.Codec`
[🔗](https://github.com/andreashasse/spectral/blob/main/lib/spectral/codec.ex#L1)

Behaviour for custom codec modules.

A custom codec lets you override the default encode/decode/schema logic for specific
types in your module. When spectra encounters a type defined in a codec module, it
calls your callbacks first. Return `{:ok, result}` to provide a custom result,
`{:error, errors}` when the data is invalid for a type your codec owns, or `:continue`
to fall through to spectra's built-in structural encoding/decoding for types your
codec does not handle.

## Usage

Add `use Spectral.Codec` to your module and implement the required callbacks.
Spectra detects codec modules by checking for `@behaviour Spectral.Codec` in
the compiled BEAM, so no extra registration is needed:

    defmodule MyGeoModule do
      use Spectral.Codec

      @opaque point :: {float(), float()}

      @impl Spectral.Codec
      def encode(_format, _caller_type_info, {:type, :point, 0}, _target_type, {x, y}, _config)
          when is_number(x) and is_number(y) do
        {:ok, [x, y]}
      end

      def encode(_format, _caller_type_info, {:type, :point, 0}, _target_type, data, _config) do
        {:error, [%Spectral.Error{type: :type_mismatch, location: [], context: %{type: {:type, :point, 0}, value: data}}]}
      end

      # Types not handled by this codec → continue to default
      def encode(_format, _caller_type_info, _type_ref, _target_type, _data, _config), do: :continue

      @impl Spectral.Codec
      def decode(_format, _caller_type_info, {:type, :point, 0}, _target_type, [x, y], _config)
          when is_number(x) and is_number(y) do
        {:ok, {x, y}}
      end

      def decode(_format, _caller_type_info, {:type, :point, 0}, _target_type, data, _config) do
        {:error, [%Spectral.Error{type: :type_mismatch, location: [], context: %{type: {:type, :point, 0}, value: data}}]}
      end

      def decode(_format, _caller_type_info, _type_ref, _target_type, _input, _config), do: :continue

      @impl Spectral.Codec
      def schema(:json_schema, _caller_type_info, {:type, :point, 0}, _target_type, _config) do
        %{type: "array", items: %{type: "number"}, minItems: 2, maxItems: 2}
      end
    end

## The `target_type` argument

The fourth argument to `encode/6`, `decode/6`, and `schema/5` is the `sp_type()`
instantiation node from the type traversal. For generic types (those with type
variables, such as `MapSet.t(elem)`) this is the reference node and carries the
concrete type-variable bindings of the current instantiation. Use
`Spectral.Type.type_args/1` to extract them for recursive encoding/decoding.

For non-generic types this argument is the resolved type definition and
`Spectral.Type.type_args/1` returns `[]`.

## Return Values

- `{:ok, result}` — Use this result instead of the default
- `{:error, errors}` — The data is invalid for a type this codec handles; `errors`
  is a list of `%Spectral.Error{}` structs
- `:continue` — This codec does not handle this type; fall through to spectra's
  built-in structural codec

The distinction between `{:error, ...}` and `:continue` matters: return `{:error, ...}`
when the data has the wrong shape for a type your codec *owns*, and `:continue` for
type references your codec does not recognise at all.

## Recursive Calls

When your codec handles a container type and needs to encode or decode its elements
according to their types, use the helper functions on this module rather than calling
`Spectral.encode/5` or `Spectral.decode/5`. The helpers preserve the runtime `config`
(cache mode, codec registry, format) across the traversal; the public `Spectral` API
would start a fresh traversal and lose that context.

    @impl Spectral.Codec
    def encode(format, caller_type_info, {:type, :wrapper, 1}, target_type, %Wrapper{value: v}, config) do
      case Spectral.Type.type_args(target_type) do
        [elem_type] ->
          case Spectral.Codec.encode(format, caller_type_info, elem_type, v, config) do
            {:ok, encoded} -> {:ok, %{"value" => encoded}}
            error -> error
          end
        [] -> {:ok, %{"value" => v}}
      end
    end

Use `Spectral.Type.type_args/1` to extract the concrete type arguments from `target_type`
when handling generic types (see the `target_type` section above).

## Global Codec Registry

To use a codec for types defined in a *different* module (e.g., a stdlib or
third-party type you cannot annotate), register it in `config/config.exs`:

    config :spectra, :codecs, %{
      {SomeLibrary, {:type, :some_type, 0}} => MyApp.Codec.SomeType
    }

The built-in codecs (`Date`, `DateTime`, `NaiveDateTime`, `Time`, `MapSet`,
`String`) are registered automatically; entries here take precedence over them.

# `decode_result`

```elixir
@type decode_result() :: {:ok, term()} | {:error, [Spectral.Error.t()]} | :continue
```

Return value for `decode/6` callback.

# `encode_result`

```elixir
@type encode_result() :: {:ok, term()} | {:error, [Spectral.Error.t()]} | :continue
```

Return value for `encode/6` callback.

# `decode`

```elixir
@callback decode(
  format :: atom(),
  caller_type_info :: Spectral.type_info(),
  target_type_ref :: Spectral.sp_type_reference(),
  target_type :: Spectral.sp_type_or_ref(),
  input :: term(),
  config :: term()
) :: decode_result()
```

Decodes `input` from `format` into the Elixir value described by `target_type_ref`.

Called by spectra when decoding a value whose type is defined in a codec module.
Return `{:error, errors}` when the input is invalid for a type your codec handles,
or `:continue` for types this codec does not recognise.

`caller_type_info` is the type info of the module driving the traversal.
`target_type` is the instantiation node from the type traversal; use
`Spectral.Type.type_args/1` to extract type-variable bindings for generic types.
Use `:spectra_type.parameters/1` on `target_type` to read `type_parameters` (only
reliable when the codec is invoked directly from a `Spectral` entry point).
`config` is the runtime config; pass `format` and `config` to `Spectral.Codec.decode/5` for recursive calls.

# `encode`

```elixir
@callback encode(
  format :: atom(),
  caller_type_info :: Spectral.type_info(),
  target_type_ref :: Spectral.sp_type_reference(),
  target_type :: Spectral.sp_type_or_ref(),
  data :: term(),
  config :: term()
) :: encode_result()
```

Encodes `data` of the given `target_type_ref` to `format`.

Called by spectra when encoding a value whose type is defined in a codec module.
Return `{:error, errors}` when the data is invalid for a type your codec handles,
or `:continue` for types this codec does not recognise.

`caller_type_info` is the type info of the module driving the traversal.
`target_type` is the instantiation node from the type traversal; use
`Spectral.Type.type_args/1` to extract type-variable bindings for generic types.
Use `:spectra_type.parameters/1` on `target_type` to read `type_parameters` (only
reliable when the codec is invoked directly from a `Spectral` entry point).
`config` is the runtime config; pass `format` and `config` to `Spectral.Codec.encode/5`,
`Spectral.Codec.decode/5`, and `Spectral.Codec.schema/4` for recursive calls within
this callback.

# `schema`
*optional* 

```elixir
@callback schema(
  format :: atom(),
  caller_type_info :: Spectral.type_info(),
  target_type_ref :: Spectral.sp_type_reference(),
  target_type :: Spectral.sp_type_or_ref(),
  config :: term()
) :: map() | :continue
```

Returns a schema map for `target_type_ref` in `format`.

This callback is optional. If not implemented, spectra will raise
`{:schema_not_implemented, module, type_ref}` when schema generation is requested
for a type owned by this codec.

Once implemented, the callback receives *every* type defined in the codec module, not
only the ones the codec handles. Give it a catch-all clause returning `:continue` for
the rest, exactly as with `encode/6` and `decode/6`, or schema generation for those
types raises a `FunctionClauseError`.

`caller_type_info` is the type info of the module driving the traversal.
`target_type` is the type node; use `:spectra_type.parameters/1` to read
`type_parameters` (only reliable when invoked directly from a `Spectral` entry point).
`config` is the runtime config; pass `format` and `config` to `Spectral.Codec.schema/4` for recursive calls.

# `decode`

```elixir
@spec decode(atom(), Spectral.type_info(), Spectral.sp_type_or_ref(), term(), term()) ::
  {:ok, term()} | {:error, [Spectral.Error.t()]}
```

Recursively decodes `input` to `type_ref` inside a codec callback.

Pass the `format` and `caller_type_info` received in your `decode/6` callback.
Preserves the runtime `config` (cache mode, codecs) across the traversal,
unlike `Spectral.decode/5` which starts a fresh traversal.

The `input` must already be a decoded term (JSON is pre-parsed by the time
codec callbacks are invoked).

Returns `{:ok, term()}` or `{:error, [Spectral.Error.t()]}`.

# `encode`

```elixir
@spec encode(atom(), Spectral.type_info(), Spectral.sp_type_or_ref(), term(), term()) ::
  {:ok, term()} | {:error, [Spectral.Error.t()]}
```

Recursively encodes `data` of `type_ref` inside a codec callback.

Pass the `format` and `caller_type_info` received in your `encode/6` callback.
Preserves the runtime `config` (cache mode, codecs) across the traversal,
unlike `Spectral.encode/5` which starts a fresh traversal.

`type_ref` may be a resolved `sp_type()` node, such as one from
`Spectral.Type.type_args/1`, or a `{:type, name, arity}` / `{:record, name}` reference,
which is looked up in `type_info`.

Returns `{:ok, term()}` (a pre-encoded term) or `{:error, [Spectral.Error.t()]}`.

# `schema`

```elixir
@spec schema(atom(), Spectral.type_info(), Spectral.sp_type_or_ref(), term()) ::
  dynamic()
```

Generates a schema map for `type_ref` inside a codec `schema/5` callback.

Pass the `caller_type_info` received in your `schema/5` callback.
Preserves the runtime `config` across the traversal. Returns a pre-encoded schema map.

---

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