# `Peri`
[🔗](https://github.com/zoedsoupe/peri/blob/v0.11.3/lib/peri.ex#L1)

Peri is a data description library for Elixir, in the spirit of Clojure's
Plumatic Schema and Metosin's Malli. A schema is plain Elixir data: atoms
like `:string`, literals like `{:literal, 42}`, tuples, maps, keyword lists,
composed however the data demands. There is no separate DSL to learn; the
schema language is Elixir itself. Peri is data, Peri is Elixir.

Because schemas are data, they are programmable: the same definition can
validate structs, coerce string params at the boundary, generate test data,
export JSON Schema, build Ecto changesets, or render Phoenix forms.

## Installation

```elixir
defp deps do
  [
    {:peri, "~> 0.11.3"} # x-release-please-version
  ]
end
```

## Quick Start

```elixir
defmodule MyApp.Schemas do
  import Peri

  defschema :user, %{
    name: {:required, :string},
    age: {:integer, gte: 18},
    role: {:enum, [:admin, :user, :guest]}
  }

  defschema :search, %{
    page: {:coerce, :integer},
    tags: {:coerce, {:list, :string}, split: ","}
  }
end

MyApp.Schemas.user(%{name: "John", age: 25, role: :user})
# => {:ok, %{name: "John", age: 25, role: :user}}

# Boundary data arrives as strings; {:coerce, ...} types it.
MyApp.Schemas.search(%{"page" => "2", "tags" => "elixir,otp"})
# => {:ok, %{page: 2, tags: ["elixir", "otp"]}}
```
## Guides

  * [Schema types and directives](types.md): the full type vocabulary,
    basic and time types, collections, enums, literals, constraints,
    coercion/encoding, `{:meta, ...}`, custom error messages, schema
    transformation with `Peri.walk/2`
  * [Validation semantics](validation.md): strict/permissive modes,
    `:cond` and `:dependent` callbacks and their arities, custom
    validators, error context, decoding and encoding wire data
  * [Data generation](generation.md): `Peri.generate/1`, constraint-aware
    generators, `gen:` overrides, property-based testing
  * [JSON Schema export](json_schema.md): `Peri.to_json_schema/1`
  * [Ecto integration](ecto.md): changesets and custom Ecto types
  * [Phoenix integration](phoenix.md): form and params bridging
  * [Schema refs](refs.md): reusable named schemas via `{:ref, ...}`

# `coerce_def`

```elixir
@type coerce_def() ::
  {:coerce, schema_def()}
  | {:coerce, schema_def(), keyword()}
  | {:coerce, coerce_source(), schema_def()}
  | {:coerce, coerce_source(), schema_def(), keyword()}
```

# `coerce_source`

```elixir
@type coerce_source() ::
  :string
  | (term() -&gt; {:ok, term()} | :error)
  | {module(), atom()}
  | {module(), atom(), [term()]}
```

# `cond_def`

```elixir
@type cond_def() ::
  {:cond, condition :: (term() -&gt; boolean()), true_branch :: schema_def(),
   else_branch :: schema_def()}
  | {:cond, condition :: (current :: term(), root :: term() -&gt; boolean()),
     true_branch :: schema_def(), else_branch :: schema_def()}
```

# `custom_def`

```elixir
@type custom_def() ::
  {:custom, validation()}
  | {:custom, {module(), atom()}}
  | {:custom, {module(), atom(), [term()]}}
```

# `default_def`

```elixir
@type default_def() ::
  {schema_def(), {:default, term()}}
  | {schema_def(), {:default, (-&gt; term())}}
  | {schema_def(), {:default, {module(), atom()}}}
```

# `dependent_def`

```elixir
@type dependent_def() ::
  {:dependent, field :: atom(), validation(), type :: schema_def()}
  | {:dependent,
     (term() -&gt;
        {:ok, schema_def() | nil}
        | {:error, template :: String.t(), context :: map() | keyword()})}
  | {:dependent,
     (current :: term(), root :: term() -&gt;
        {:ok, schema_def() | nil}
        | {:error, template :: String.t(), context :: map() | keyword()})}
  | {:dependent, {module(), fun :: atom()}}
  | {:dependent, {module(), fun :: atom(), args :: list()}}
```

# `encode_def`

```elixir
@type encode_def() ::
  {schema_def(), {:encode, (term() -&gt; term())}}
  | {schema_def(), {:encode, {module(), atom()}}}
  | {schema_def(), {:encode, {module(), atom(), [term()]}}}
```

# `explicit_schema_def`

```elixir
@type explicit_schema_def() ::
  {:schema, schema()}
  | {:schema, map_schema(), {:additional_keys, schema_def()}}
```

# `float_def`

```elixir
@type float_def() ::
  :float | {:float, numeric_option(float()) | [numeric_option(float())]}
```

# `int_def`

```elixir
@type int_def() ::
  :integer | {:integer, numeric_option(integer()) | [numeric_option(integer())]}
```

# `literal`

```elixir
@type literal() :: integer() | float() | atom() | String.t() | boolean()
```

# `map_schema`

```elixir
@type map_schema() :: %{required(String.t() | atom()) =&gt; schema_def()}
```

# `numeric_option`

```elixir
@type numeric_option(type) ::
  {:eq, type}
  | {:neq, type}
  | {:lt, type}
  | {:lte, type}
  | {:gt, type}
  | {:gte, type}
  | {:range, {min :: type, max :: type}}
```

# `schema`

```elixir
@type schema() :: schema_def() | map_schema() | [{atom(), schema_def()}]
```

# `schema_def`

```elixir
@type schema_def() ::
  :any
  | :atom
  | :atom!
  | :boolean
  | :map
  | :pid
  | {:either, {schema_def(), schema_def()}}
  | {:oneof, [schema_def()]}
  | {:required, schema_def()}
  | {:meta, schema_def(), keyword()}
  | {:ref, atom()}
  | {:ref, {module(), atom()}}
  | {:multi, atom(), %{optional(term()) =&gt; schema_def()}}
  | {:enum, [term()]}
  | {:enum, [term()], keyword()}
  | {:list, schema_def()}
  | {:map, schema_def()}
  | {:map, key_type :: schema_def(), value_type :: schema_def()}
  | {:tuple, [schema_def()]}
  | {:literal, literal()}
  | time_def()
  | string_def()
  | int_def()
  | float_def()
  | default_def()
  | transform_def()
  | custom_def()
  | coerce_def()
  | encode_def()
```

# `string_def`

```elixir
@type string_def() :: :string | {:string, string_option() | [string_option()]}
```

# `string_option`

```elixir
@type string_option() ::
  {:regex, Regex.t()}
  | {:eq, String.t()}
  | {:min, integer()}
  | {:max, integer()}
```

# `time_def`

```elixir
@type time_def() :: :time | :date | :datetime | :naive_datetime | :duration
```

# `transform_def`

```elixir
@type transform_def() ::
  {schema_def(), {:transform, (term() -&gt; term()) | (term(), term() -&gt; term())}}
  | {schema_def(), {:transform, {module(), atom()}}}
  | {schema_def(), {:transform, {module(), atom(), [term()]}}}
```

# `validation`

```elixir
@type validation() :: (term() -&gt; validation_result())
```

# `validation_result`

```elixir
@type validation_result() ::
  :ok | {:error, template :: String.t(), context :: map() | keyword()}
```

# `conforms?`

Checks if the given data conforms to the specified schema.

## Parameters

  - `schema`: The schema definition to validate against.
  - `data`: The data to be validated.

## Options

  - `:mode` - Validation mode. Can be `:strict` (default) or `:permissive`.
    - `:strict` - Only fields defined in the schema are returned.
    - `:permissive` - All fields from the input data are preserved.

## Returns

  - `true` if the data conforms to the schema.
  - `false` if the data does not conform to the schema.

## Examples

    iex> schema = %{name: :string, age: :integer}
    iex> data = %{name: "Alice", age: 30}
    iex> Peri.conforms?(schema, data)
    true

    iex> invalid_data = %{name: "Alice", age: "thirty"}
    iex> Peri.conforms?(schema, invalid_data)
    false

# `decode`

```elixir
@spec decode(schema(), data :: term(), keyword()) :: {:ok, term()} | {:error, term()}
```

Decodes wire data against a schema.

Coercion is a schema directive, so decoding is exactly `validate/3`:
`{:coerce, source, target}` fields convert their `source` representation
into the `target` type, and values already matching the target pass through
unchanged.

Accepts the same options as `validate/3` and returns the same shape.

## Examples

    iex> schema = %{page: {:coerce, :string, :integer}}
    iex> Peri.decode(schema, %{"page" => "2"})
    {:ok, %{page: 2}}

    iex> schema = %{page: {:coerce, :string, :integer}}
    iex> Peri.decode(schema, %{page: 2})
    {:ok, %{page: 2}}

# `defschema`
*macro* 

Defines a schema with a given name and schema definition.

## Examples

    defmodule MySchemas do
      import Peri

      defschema :user, %{
        name: :string,
        age: :integer,
        email: {:required, :string}
      }

      # With permissive mode
      defschema :flexible_user, %{
        name: :string,
        email: {:required, :string}
      }, mode: :permissive

      # With metadata (field-level and schema-level)
      defschema :documented_user, %{
        email: {:meta, {:required, :string}, doc: "Login email", example: "a@b.io"}
      }, title: "User", description: "Account holder"
    end

    # Schema-level metadata is accessible via __schema_meta__/1:
    MySchemas.__schema_meta__(:documented_user)
    # => [title: "User", description: "Account holder"]

    user_data = %{name: "John", age: 30, email: "john@example.com"}
    MySchemas.user(user_data)
    # => {:ok, %{name: "John", age: 30, email: "john@example.com"}}

    invalid_data = %{name: "John", age: 30}
    MySchemas.user(invalid_data)
    # => {:error, [email: "is required"]}

    # Permissive mode preserves extra fields
    flexible_data = %{name: "John", email: "john@example.com", role: "admin"}
    MySchemas.flexible_user(flexible_data)
    # => {:ok, %{name: "John", email: "john@example.com", role: "admin"}}

# `encode`

```elixir
@spec encode(schema(), data :: term(), keyword()) :: {:ok, term()} | {:error, term()}
```

Encodes data against a schema, producing its wire representation.

Validates `data` against the schema (target types), then applies the
reverse of each codec directive:

  * `{:coerce, source, target}` - built-in `:string` sources render with
    `to_string/1` (`to_iso8601/1` for date/time structs); when the
    `encode:` opt is given, that function/MFA is applied instead. Custom
    function/MFA sources without an `encode:` opt pass the validated value
    through unchanged.
  * `{type, {:encode, fun}}` - applies `fun` (or MFA) to the validated
    value. This directive is inert under `validate/3` and `decode/3`.
  * `{:transform, fun}` - skipped; transforms are decode-only.

Accepts the same options as `validate/3` and returns the same shape.

## Examples

    iex> schema = %{page: {:coerce, :string, :integer}}
    iex> Peri.encode(schema, %{page: 2})
    {:ok, %{page: "2"}}

    iex> schema = %{upcased: {:string, {:encode, &String.downcase/1}}}
    iex> Peri.encode(schema, %{upcased: "HELLO"})
    {:ok, %{upcased: "hello"}}

# `except`

```elixir
@spec except(map_schema(), [atom() | String.t()]) ::
  {:ok, map_schema()} | {:error, [Peri.Error.t()]}
```

Drops the given top-level keys from a map schema.

Keys not present in the schema are ignored. The resulting schema is checked
with `validate_schema/1`, returning its error tuple when invalid.

## Examples

    iex> Peri.except(%{name: :string, age: :integer}, [:age])
    {:ok, %{name: :string}}

    iex> Peri.except(%{name: :string}, [:missing])
    {:ok, %{name: :string}}

# `from_json_schema`

```elixir
@spec from_json_schema(map(), [opt]) :: {:ok, schema()} | {:error, term()}
when opt: {:keys, :strings | :atoms | :atoms!}
```

Decodes a JSON Schema (Draft 7) map into a Peri schema.

Returns `{:ok, schema}` if the resulting Peri schema is valid, otherwise
`{:error, errors}`.

# `generate`

Generates sample data based on the given schema definition using `StreamData`.

This function validates the schema first, and if the schema is valid, it uses the
`Peri.Generatable.gen/1` function to generate data according to the schema.

Note that this function returns a `Stream`, so you traverse easily the data generations.

## Parameters

  - `schema`: The schema definition to generate data for.

## Returns

  - `{:ok, stream}` if the data is successfully generated.
  - `{:error, errors}` if there are validation errors in the schema.

## Examples

    iex> schema = %{name: :string, age: {:integer, {:range, {18, 65}}}}
    iex> {:ok, stream} = Peri.generate(schema)
    iex> [data] = Enum.take(stream, 1)
    iex> is_map(data)
    true
    iex> data[:age] in 18..65
    true

# `get_enumerable_value`

# `is_enumerable`
*macro* 

Checks if the given data is an enumerable, specifically a map or a list.

## Parameters

  - `data`: The data to check.

## Examples

    iex> is_enumerable(%{})
    true

    iex> is_enumerable([])
    true

    iex> is_enumerable(123)
    false

    iex> is_enumerable("string")
    false

# `is_numeric`
*macro* 

Checks if the given data is a numeric value, specifically a integer or a float.

## Parameters

  - `data`: The data to check.

## Examples

    iex> is_numeric(123)
    true

    iex> is_numeric(0xFF)
    true

    iex> is_numeric(12.12)
    true

    iex> is_numeric("string")
    false

    iex> is_numeric(%{})
    false

# `is_numeric_type`
*macro* 

Checks if the given type as an atom is a numeric (integer or float).

## Parameters

  - `data`: The data to check.

## Examples

    iex> is_numeric(:integer)
    true

    iex> is_numeric(:float)
    true

    iex> is_numeric(:list)
    false

    iex> is_numeric({:enum, _})
    false

# `is_of_numeric_type`
*macro* 

# `is_type_with_multiple_options`
*macro* 

# `merge`

```elixir
@spec merge(map_schema(), map_schema()) ::
  {:ok, map_schema()} | {:error, [Peri.Error.t()]}
```

Deep merges two map schemas into a new one.

When both schemas define the same key as a plain map (a nested schema), the
nested schemas are merged recursively. Otherwise the value from the right
side wins: type tuples and directives are never merged structurally.

The resulting schema is checked with `validate_schema/1`, returning its
error tuple when the composed schema is invalid.

## Examples

    iex> Peri.merge(%{name: :string}, %{age: :integer})
    {:ok, %{name: :string, age: :integer}}

    iex> Peri.merge(%{name: :string}, %{name: :atom})
    {:ok, %{name: :atom}}

    iex> Peri.merge(%{user: %{name: :string}}, %{user: %{age: :integer}})
    {:ok, %{user: %{name: :string, age: :integer}}}

# `put_in_enum`

Helper function to put a value into an enum, handling
not only maps and keyword lists but also structs.

## Examples

    iex> Peri.put_in_enum(%{}, :hello, "world")
    iex> Peri.put_in_enum(%{}, "hello", "world")
    iex> Peri.put_in_enum(%User{}, :hello, "world")
    iex> Peri.put_in_enum([], :hello, "world")

# `select`

```elixir
@spec select(map_schema(), [atom() | String.t()]) ::
  {:ok, map_schema()} | {:error, [Peri.Error.t()]}
```

Keeps only the given top-level keys of a map schema.

Keys not present in the schema are ignored. The resulting schema is checked
with `validate_schema/1`, returning its error tuple when invalid.

## Examples

    iex> Peri.select(%{name: :string, age: :integer}, [:name])
    {:ok, %{name: :string}}

    iex> Peri.select(%{name: :string}, [:name, :missing])
    {:ok, %{name: :string}}

# `to_changeset!`

```elixir
@spec to_changeset!(schema(), attrs :: map()) :: Ecto.Changeset.t()
```

Converts a `Peri.schema()` definition to an Ecto [schemaless changesets](https://hexdocs.pm/ecto/Ecto.Changeset.html#module-schemaless-changesets).

# `to_json_schema`

```elixir
@spec to_json_schema(schema(), Peri.JSONSchema.Encoder.opts()) :: map()
```

Converts a Peri schema into a JSON Schema (Draft 7) map.

Reads `{:meta, type, opts}` annotations and emits `title`, `description`,
`examples`, `deprecated`. Dynamic types degrade per `:on_unsupported`
(`:omit | :true_schema | :raise`, default `:omit`).

Pass `:exclude_meta_keys` to drop annotation keywords from the output —
commonly `[:default]` when the consumer-facing schema should not surface
validation defaults.

## Examples

    iex> Peri.to_json_schema(%{name: {:required, :string}})
    %{"type" => "object", "properties" => %{"name" => %{"type" => "string"}}, "required" => ["name"]}

    iex> Peri.to_json_schema({:integer, {:default, 0}}, exclude_meta_keys: [:default])
    %{"type" => "integer"}

# `validate`

Validates a given data map against a schema with options.

Returns `{:ok, data}` if the data is valid according to the schema, or `{:error, errors}` if there are validation errors.

## Parameters

  - schema: The schema definition map.
  - data: The data map to be validated.
  - opts: Options for validation.

## Options

  - `:mode` - Validation mode. Can be `:strict` (default) or `:permissive`.
    - `:strict` - Only fields defined in the schema are returned.
    - `:permissive` - All fields from the input data are preserved.

## Examples

    schema = %{name: :string, age: :integer}
    data = %{name: "John", age: 30, extra: "field"}

    # Strict mode (default)
    Peri.validate(schema, data)
    # => {:ok, %{name: "John", age: 30}}

    # Permissive mode
    Peri.validate(schema, data, mode: :permissive)
    # => {:ok, %{name: "John", age: 30, extra: "field"}}

# `validate_schema`

Validates a schema definition to ensure it adheres to the expected structure and types.

This function can handle both simple and complex schema definitions, including nested schemas, custom validation functions, and various type constraints.

## Parameters

  - `schema` - The schema definition to be validated. It can be a map or a keyword list representing the schema.

## Returns

  - `{:ok, schema}` - If the schema is valid, returns the original schema.
  - `{:error, errors}` - If the schema is invalid, returns an error tuple with detailed error information.

## Examples

  Validating a simple schema:

  ```elixir
  schema = %{
    name: :string,
    age: :integer,
    email: {:required, :string}
  }
  assert {:ok, ^schema} = validate_schema(schema)
  ```

  Validating a nested schema:

  ```elixir
  schema = %{
    user: %{
      name: :string,
      profile: %{
        age: {:required, :integer},
        email: {:required, :string}
      }
    }
  }
  assert {:ok, ^schema} = validate_schema(schema)
  ```

  Handling invalid schema definition:

  ```elixir
  schema = %{
    name: :str,
    age: :integer,
    email: {:required, :string}
  }
  assert {:error, _errors} = validate_schema(schema)
  ```

# `walk`

```elixir
@spec walk(schema(), Peri.Walker.walker_fun()) :: schema()
```

Depth-first rewrite of a schema tree.

The callback is invoked on every subtree (pre-order). It must return either
`{:cont, new_node}` to replace the node and continue, or `:drop` to remove it
(only valid for values inside a map or keyword schema).

Building block for transforms like "make every field optional" or "strip
internal-only fields from a public DTO". See `Peri.Walker` for details.

## Examples

    iex> schema = %{name: {:required, :string}, age: {:required, :integer}}
    iex> Peri.walk(schema, fn
    ...>   {:required, t} -> {:cont, t}
    ...>   other -> {:cont, other}
    ...> end)
    %{name: :string, age: :integer}

---

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