Spectral.Codec behaviour (Spectral v0.9.2)

View Source

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, MyGeoModule, {:type, :point, 0}, {x, y}, _sp_type, _params)
      when is_number(x) and is_number(y) do
    {:ok, [x, y]}
  end

  def encode(_format, MyGeoModule, {:type, :point, 0}, data, _sp_type, _params) 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, _module, _type_ref, _data, _sp_type, _params), do: :continue

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

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

  def decode(_format, _module, _type_ref, _input, _sp_type, _params), do: :continue

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

The params argument

The sixth argument to encode/6 and decode/6, and the fifth to schema/5, is the value of the type_parameters key in the spectral attribute placed before the type definition, or :undefined if no such attribute is present. It is a static, per-type configuration value — it is not related to Erlang type variables.

The sp_type argument

The fifth argument to encode/6 and decode/6, and the fourth to schema/5, is the sp_type() instantiation node from the type traversal. For generic types (those with type variables, such as dict:dict(key, value)) 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.

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 via the application environment:

Application.put_env(:spectra, :codecs, %{
  {DateTime, {:type, :t, 0}} => Spectral.Codec.DateTime
})

Summary

Types

Return value for decode/6 callback.

Return value for encode/6 callback.

Callbacks

Decodes input from format into the Elixir value described by type_ref (defined in module).

Encodes data of the given type_ref (defined in module) to format.

Returns a schema map for type_ref (defined in module) in format.

Types

decode_result()

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

Return value for decode/6 callback.

encode_result()

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

Return value for encode/6 callback.

Callbacks

decode(format, module, type_ref, input, sp_type, params)

@callback decode(
  format :: atom(),
  module :: module(),
  type_ref :: Spectral.sp_type_reference(),
  input :: term(),
  sp_type :: term(),
  params :: term()
) :: decode_result()

Decodes input from format into the Elixir value described by type_ref (defined in module).

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.

sp_type is the instantiation node from the type traversal (see module doc). params is the value of type_parameters from the spectral attribute on the type definition, or :undefined if absent.

encode(format, module, type_ref, data, sp_type, params)

@callback encode(
  format :: atom(),
  module :: module(),
  type_ref :: Spectral.sp_type_reference(),
  data :: term(),
  sp_type :: term(),
  params :: term()
) :: encode_result()

Encodes data of the given type_ref (defined in module) 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.

sp_type is the instantiation node from the type traversal (see module doc). params is the value of type_parameters from the spectral attribute on the type definition, or :undefined if absent.

schema(format, module, type_ref, sp_type, params)

(optional)
@callback schema(
  format :: atom(),
  module :: module(),
  type_ref :: Spectral.sp_type_reference(),
  sp_type :: term(),
  params :: term()
) :: map()

Returns a schema map for type_ref (defined in module) 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.

sp_type is the instantiation node from the type traversal (see module doc). params is the value of type_parameters from the spectral attribute on the type definition, or :undefined if absent.