Spectral (Spectral v0.12.0)

View Source

Elixir wrapper for the Erlang spectra library.

Provides idiomatic Elixir interfaces for encoding, decoding, and schema generation based on type specifications.

API

All functions are designed to work well with Elixir's pipe and with operators:

%Person{name: "Alice", age: 30}
|> Spectral.encode!(Person, :t)
|> send_response()

with {:ok, json} <- Spectral.encode(%Person{name: "Alice"}, Person, :t) do
  send_response(json)
end

Summary

Types

Options for schema/4.

A spectra type structure or type reference. sp_type() is an opaque Erlang record.

A reference to a named type {:type, name, arity} or record {:record, name}.

Spectra type information for a module. Alias for :spectra.type_info().

Functions

Sets up the Spectral macros and injects __spectra_type_info__/0.

Decodes data from the specified format.

Decodes data from the specified format, raising on error.

Encodes data to the specified format.

Encodes data to the specified format, raising on error.

Generates a schema for the specified type.

Generates a schema for the specified type, with options.

Adds documentation metadata for a type.

Types

decode_option()

@type decode_option() :: :pre_decoded | {:pre_decoded, boolean()}

Options for decode/5 and decode!/5.

  • :pre_decoded - Accept an already-decoded JSON term as input, skipping the JSON parsing step. Equivalent to {:pre_decoded, true}.
  • {:pre_decoded, boolean()} - Explicit boolean form; false gives the default behaviour.

encode_option()

@type encode_option() :: :pre_encoded | {:pre_encoded, boolean()}

Options for encode/5 and encode!/5.

  • :pre_encoded - Skip the final JSON serialization step and return the intermediate JSON term (a map/list) instead of iodata. Equivalent to {:pre_encoded, true}.
  • {:pre_encoded, boolean()} - Explicit boolean form; false gives the default behaviour.

schema_option()

@type schema_option() :: :pre_encoded | {:pre_encoded, boolean()}

Options for schema/4.

  • :pre_encoded - Skip the final JSON serialization step and return a map instead of iodata. Equivalent to {:pre_encoded, true}.
  • {:pre_encoded, boolean()} - Explicit boolean form; false gives the default behaviour.

sp_type_or_ref()

@type sp_type_or_ref() :: :spectra.sp_type_or_ref()

A spectra type structure or type reference. sp_type() is an opaque Erlang record.

sp_type_reference()

@type sp_type_reference() :: {:type, atom(), non_neg_integer()} | {:record, atom()}

A reference to a named type {:type, name, arity} or record {:record, name}.

type_info()

@type type_info() :: :spectra.type_info()

Spectra type information for a module. Alias for :spectra.type_info().

Functions

__using__(opts)

(macro)

Sets up the Spectral macros and injects __spectra_type_info__/0.

When you use Spectral, the spectral/1 macro is imported and a __spectra_type_info__/0 function is injected that Spectral's encode, decode, and schema functions use internally.

Annotating Types

Place spectral/1 immediately before a @type to attach documentation to generated JSON schemas and OpenAPI component schemas:

defmodule Person do
  use Spectral

  defstruct [:name, :age]

  spectral title: "Person", description: "A person record"
  @type t :: %Person{name: String.t(), age: non_neg_integer()}
end

Types without a spectral call will not have title/description in their schemas. See spectral/1 for the full list of supported annotation fields.

Annotating Functions (Endpoint Documentation)

Place spectral/1 immediately before a @spec to attach endpoint metadata, which Spectral.OpenAPI.endpoint/5 reads automatically:

defmodule MyController do
  use Spectral

  spectral summary: "Get user", description: "Returns a user by ID"
  @spec show(map(), map()) :: map()
  def show(_conn, _params), do: %{}
end

endpoint = Spectral.OpenAPI.endpoint(:get, "/users/{id}", MyController, :show, 2)

decode(data, module, type_ref, format \\ :json, opts \\ [])

@spec decode(dynamic(), module() | type_info(), atom() | sp_type_or_ref(), atom(), [
  decode_option()
]) ::
  {:ok, dynamic()} | {:error, [Spectral.Error.t()]}

Decodes data from the specified format.

Parameters

  • data - The data to decode (binary for JSON, string for string format; or a JSON term when :pre_decoded option is set)
  • module - Module containing the type definition
  • type_ref - Type reference (typically an atom like :t)
  • format - Format to decode from (default: :json)
  • opts - Options list (default: []). Supported options:
    • :pre_decoded - Accept an already-decoded JSON term as input, skipping JSON parsing.

Returns

  • {:ok, dynamic()} - Decoded data on success
  • {:error, [%Spectral.Error{}]} - List of errors on failure

Examples

iex> ~s({"name":"Alice","age":30,"address":{"street":"Ystader Straße", "city": "Berlin"}})
...> |> Spectral.decode(Person, :t)
{:ok, %Person{age: 30, name: "Alice", address: %Person.Address{street: "Ystader Straße", city: "Berlin"}}}

iex> ~s({"name":"Alice"})
...> |> Spectral.decode(Person, :t)
{:ok, %Person{age: nil, name: "Alice", address: nil}}

iex> ~s({"name":"Alice","age":30,"extra_field":"ignored"})
...> |> Spectral.decode(Person, :t)
{:ok, %Person{age: 30, name: "Alice", address: nil}}

iex> Spectral.decode(%{"name" => "Alice", "age" => 30}, Person, :t, :json, [:pre_decoded])
{:ok, %Person{age: 30, name: "Alice", address: nil}}

decode!(data, module, type_ref, format \\ :json, opts \\ [])

@spec decode!(dynamic(), module() | type_info(), atom() | sp_type_or_ref(), atom(), [
  decode_option()
]) ::
  dynamic()

Decodes data from the specified format, raising on error.

Like decode/5 but raises Spectral.Error instead of returning an error tuple.

Parameters

  • data - The data to decode (binary for JSON, string for string format; or a JSON term when :pre_decoded option is set)
  • module - Module containing the type definition
  • type_ref - Type reference (typically an atom like :t)
  • format - Format to decode from (default: :json)
  • opts - Options list (default: []). Supported options:
    • :pre_decoded - Accept an already-decoded JSON term as input, skipping JSON parsing.

Returns

  • dynamic() - Decoded data on success

Raises

Examples

iex> ~s({"name":"Alice","age":30})
...> |> Spectral.decode!(Person, :t)
%Person{age: 30, name: "Alice", address: nil}

encode(data, module, type_ref, format \\ :json, opts \\ [])

@spec encode(dynamic(), module() | type_info(), atom() | sp_type_or_ref(), atom(), [
  encode_option()
]) ::
  {:ok, iodata() | dynamic()} | {:error, [Spectral.Error.t()]}

Encodes data to the specified format.

Parameters

  • data - The data to encode
  • module - Module containing the type definition
  • type_ref - Type reference (typically an atom like :t)
  • format - Format to encode to (default: :json)
  • opts - Options list (default: []). Supported options:
    • :pre_encoded - Return the intermediate JSON term (map/list) instead of iodata.

Returns

  • {:ok, iodata()} - Encoded data on success (default)
  • {:ok, dynamic()} - Encoded data as a JSON term when :pre_encoded option is set
  • {:error, [%Spectral.Error{}]} - List of errors on failure

Examples

iex> person = %Person{name: "Alice", age: 30, address: %Person.Address{street: "Ystader Straße", city: "Berlin"}}
...> with {:ok, json} <- Spectral.encode(person, Person, :t) do
...>  IO.iodata_to_binary(json)
...> end
~s({"address":{"city":"Berlin","street":"Ystader Straße"},"age":30,"name":"Alice"})

iex> {:ok, json} = %Person{name: "Alice"} |> Spectral.encode(Person, :t)
iex> IO.iodata_to_binary(json)
~s({"name":"Alice"})

iex> {:ok, term} = %Person{name: "Alice", age: 30} |> Spectral.encode(Person, :t, :json, [:pre_encoded])
iex> term["name"]
"Alice"

encode!(data, module, type_ref, format \\ :json, opts \\ [])

@spec encode!(dynamic(), module() | type_info(), atom() | sp_type_or_ref(), atom(), [
  encode_option()
]) ::
  iodata() | dynamic()

Encodes data to the specified format, raising on error.

Like encode/5 but raises Spectral.Error instead of returning an error tuple.

Parameters

  • data - The data to encode
  • module - Module containing the type definition
  • type_ref - Type reference (typically an atom like :t)
  • format - Format to encode to (default: :json)
  • opts - Options list (default: []). Supported options:
    • :pre_encoded - Return the intermediate JSON term (map/list) instead of iodata.

Returns

  • iodata() - Encoded data on success (default)
  • dynamic() - Encoded data as a JSON term when :pre_encoded option is set

Raises

Examples

iex> %Person{name: "Alice", age: 30}
...> |> Spectral.encode!(Person, :t)
...> |> IO.iodata_to_binary()
~s({"age":30,"name":"Alice"})

schema(module, type_ref, format \\ :json_schema)

@spec schema(module() | type_info(), atom() | sp_type_or_ref(), atom()) :: iodata()

Generates a schema for the specified type.

Parameters

  • module - Module containing the type definition
  • type_ref - Type reference (typically an atom like :t)
  • format - Schema format (default: :json_schema)

Returns

  • iodata() - Generated schema

Examples

iex> schemadata = Spectral.schema(Person, :t)
iex> is_binary(IO.iodata_to_binary(schemadata))
true

schema(module, type_ref, format, opts)

@spec schema(module() | type_info(), atom() | sp_type_or_ref(), atom(), [
  schema_option()
]) ::
  iodata() | dynamic()

Generates a schema for the specified type, with options.

Like schema/3 but accepts an options list.

Parameters

  • module - Module containing the type definition
  • type_ref - Type reference (typically an atom like :t)
  • format - Schema format (default: :json_schema)
  • opts - Options list. Supported options:
    • :pre_encoded - Return a map instead of iodata, skipping JSON encoding.

Returns

  • iodata() - Generated schema (default)
  • dynamic() - Schema as a map when :pre_encoded option is set

Examples

iex> schema = Spectral.schema(Person, :t, :json_schema, [:pre_encoded])
iex> is_map(schema)
true

sp_function_spec(args \\ [])

(macro)

sp_function_spec(record, args)

(macro)

spectral(metadata)

(macro)

Adds documentation metadata for a type.

Use this macro immediately before a @type definition to document it. The line number is automatically captured to pair the documentation with the correct type.

Example

defmodule Person do
  use Spectral

  spectral title: "Person", description: "A person record"
  @type t :: %Person{name: String.t()}
end

Fields — before a @type

  • title - A short title for the type
  • description - A detailed description
  • deprecated - Whether the type is deprecated (boolean)
  • examples - Example values (list)
  • examples_function - {module, function_name, args} tuple; called at schema generation time to produce examples. The function must be exported.
  • type_parameters - Static configuration forwarded as the params argument to Spectral.Codec callbacks for this type (any term)
  • only - List of field name atoms to include when encoding, decoding, and generating schemas. Fields not in the list are silently dropped. For Elixir structs, excluded fields are filled from the struct's defaults on decode.

Fields — before a @spec

  • summary - Short one-line summary of the function / endpoint
  • description - A detailed description
  • deprecated - Whether the function is deprecated (boolean)