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

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

# `decode_option`

```elixir
@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`

```elixir
@type encode_option() ::
  :pre_encoded | {:pre_encoded, boolean()} | {:nil_fields, :null | :omit}
```

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.
- `{:nil_fields, :null | :omit}` - For this call only, write a `nil` field whose type allows
  `nil` as JSON `null` (`:null`) or leave it out (`:omit`), nested values included. The
  schema does not change; it follows `config :spectra, :nil_fields`. Raises `ArgumentError`
  when the option would break that schema: `:null` under `:omit` (no `null` allowed) and
  `:omit` under `:null` (the field is required).

# `schema_option`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

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

# `__using__`
*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`

```elixir
@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!`

```elixir
@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

- `Spectral.Error` - If decoding fails

## Examples

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

# `encode`

```elixir
@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.
    JSON null is `nil` in this term, so Elixir JSON libraries encode it as `null`.
  - `nil_fields: :null | :omit` - Write `nil` fields as `null` or leave them out, for this
    call only. See `t:encode_option/0`.

## 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!`

```elixir
@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.
  - `nil_fields: :null | :omit` - Write `nil` fields as `null` or leave them out, for this
    call only. See `t:encode_option/0`.

## Returns

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

## Raises

- `Spectral.Error` - If encoding fails

## Examples

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

# `schema`

```elixir
@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`

```elixir
@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`
*macro* 

# `sp_function_spec`
*macro* 

# `spectral`
*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.
- `field_aliases` - Map of field name atoms (or integers for integer map keys) to JSON
  key binaries, e.g. `%{first_name: "firstName"}`. Applies to struct fields and map
  literal keys. Applied after `only` filtering.

## Fields — before a `@spec`

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

---

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