Types Reference

View Source

Peri provides a comprehensive set of built-in types for schema validation.

Basic Types

TypeDescriptionExample
:anyAccepts any value:any
:atomValidates atoms:atom
:stringValidates binary strings:string
:integerValidates integers:integer
:floatValidates floats:float
:booleanValidates booleans:boolean
:mapValidates maps (no content validation):map
:pidValidates process identifiers:pid

Time Types

TypeDescriptionExample
:dateValidates %Date{}:date
:timeValidates %Time{}:time
:datetimeValidates %DateTime{}:datetime
:naive_datetimeValidates %NaiveDateTime{}:naive_datetime
:durationValidates %Duration{}:duration

Collection Types

TypeDescriptionExample
{:list, type}List of elements of specified type{:list, :string}
{:list, type, opts}List with constraints (:min, :max, :unique){:list, :string, [min: 1, max: 10, unique: true]}
{:map, type}Map with values of specified type{:map, :integer}
{:map, key_type, value_type}Map with typed keys and values{:map, :atom, :string}
{:tuple, types}Tuple with elements of specified types{:tuple, [:float, :float]}
{:schema, map_schema, {:additional_keys, type}}Map with some strictly defined fields, with extras under a different type{:schema, %{main: :string}, {:additional_keys, :integer}}

String Constraints

TypeDescriptionExample
{:string, {:regex, regex}}String matching regex pattern{:string, {:regex, ~r/^\w+$/}}
{:string, {:eq, value}}String equal to value{:string, {:eq, "exact"}}
{:string, {:min, length}}String with minimum length{:string, {:min, 3}}
{:string, {:max, length}}String with maximum length{:string, {:max, 50}}
{:string, [...options]}String with multiple options{:string, [min: 8, max: 64, regex: ~r/^[a-zA-Z0-9-]+$/]}

Integer Constraints

TypeDescriptionExample
{:integer, {:eq, value}}Integer equal to value{:integer, {:eq, 42}}
{:integer, {:neq, value}}Integer not equal to value{:integer, {:neq, 0}}
{:integer, {:gt, value}}Integer greater than value{:integer, {:gt, 0}}
{:integer, {:gte, value}}Integer greater than or equal{:integer, {:gte, 18}}
{:integer, {:lt, value}}Integer less than value{:integer, {:lt, 100}}
{:integer, {:lte, value}}Integer less than or equal{:integer, {:lte, 99}}
{:integer, {:range, {min, max}}}Integer within range (inclusive){:integer, {:range, {18, 65}}}
{:integer, {:multiple_of, n}}Integer divisible by n{:integer, {:multiple_of, 5}}
{:integer, [...options]}Integer with multiple options{:integer, [gt: 12, lte: 96]}

Float Constraints

TypeDescriptionExample
{:float, {:eq, value}}Float equal to value{:float, {:eq, 3.1415}}
{:float, {:neq, value}}Float not equal to value{:float, {:neq, 1.9}}
{:float, {:gt, value}}Float greater than value{:float, {:gt, 1.0}}
{:float, {:gte, value}}Float greater than or equal{:float, {:gte, 9.12}}
{:float, {:lt, value}}Float less than value{:float, {:lt, 10.0}}
{:float, {:lte, value}}Float less than or equal{:float, {:lte, 99.999}}
{:float, {:range, {min, max}}}Float within range (inclusive){:float, {:range, {8.3, 15.3}}}
{:float, {:multiple_of, n}}Float divisible by n{:float, {:multiple_of, 0.25}}
{:float, [...options]}Float with multiple options{:float, [gt: 1.52, lte: 29.123]}

Choice Types

TypeDescriptionExample
{:enum, choices}Value must be one of choices{:enum, [:admin, :user, :guest]}
{:literal, value}Value must exactly match{:literal, :active}
{:either, {type1, type2}}Value matches either type{:either, {:string, :integer}}
{:oneof, types}Value matches one of types{:oneof, [:string, :integer, :atom]}

Modifiers

TypeDescriptionExample
{:required, type}Field is required{:required, :string}
{type, {:default, value}}Default value if missing{:string, {:default, "unknown"}}
{type, {:default, fun}}Default from function{:integer, {:default, &System.system_time/0}}
{type, {:default, {m, f}}}Default from MFA{:string, {:default, {MyMod, :get_default}}}
{type, {:transform, fun}}Transform value{:string, {:transform, &String.upcase/1}}
{type, {:transform, {m, f}}}Transform with MFA{:string, {:transform, {MyMod, :clean}}}
{:meta, type, opts}Attach metadata, passthrough validation{:meta, {:required, :string}, doc: "Login email", example: "a@b.io"}
{:ref, atom}Reference a schema in the same module{:list, {:ref, :tree}}
{:ref, {Mod, atom}}Reference a schema in another module{:ref, {OtherMod, :node}}
{:multi, field, branches}Tagged union dispatched on field{:multi, :type, %{"circle" => %{r: :float}, "rect" => %{w: :float}}}

Schema Metadata

The {:meta, type, opts} wrapper attaches documentation/tooling info to a field without affecting validation. Blessed keys: :doc, :title, :description, :example/:examples, :deprecated. The JSON Schema encoder also recognises :default, :format, :pattern, :read_only, :write_only, :content_encoding, and :content_media_type (see pages/json_schema.md). User keys are preserved opaquely for non-encoder tooling.

defschema accepts schema-level meta opts (any non-validation key), exposed via the generated __schema_meta__/1:

defmodule MySchemas do
  import Peri

  defschema :user, %{
    email: {:meta, {:required, :string}, doc: "Login email", example: "a@b.io"},
    age: {:meta, {:integer, gte: 0}, description: "Years"}
  }, title: "User", description: "Account holder"
end

MySchemas.__schema_meta__(:user)
# => [title: "User", description: "Account holder"]

Tagged Unions (:multi)

{:multi, dispatch_field, branches} is sugar for a discriminated union: the value is dispatched on dispatch_field, then validated against the matching branch schema. Errors localize to the chosen branch, so failures read as shape.radius: invalid rather than "didn't match any of N schemas".

%{
  shape: {:multi, :type, %{
    "circle" => %{type: {:required, :string}, radius: {:required, :float}},
    "rect"   => %{type: {:required, :string}, w: {:required, :float}, h: {:required, :float}}
  }}
}

A missing dispatch field, or a value not present in branches, surfaces as a clear validation error listing the known tags. JSON Schema export emits oneOf plus a discriminator annotation; data generation samples a branch and merges the dispatch tag into the generated value.

For untagged "any of these types" semantics, use {:oneof, types} instead.

Custom Validation

TypeDescriptionExample
{:custom, callback}Custom validation function{:custom, &validate_email/1}
{:custom, {mod, fun}}Custom validation MFA{:custom, {MyMod, :validate}}
{:custom, {mod, fun, args}}Custom validation with args{:custom, {MyMod, :validate, []}}

Custom Error Messages

Override the default error template for any field using the error: opt in its options list. Accepted forms: a static string, or an MFA {mod, fun, args} that receives the %Peri.Error{} prepended to args and returns a string.

FormDescriptionExample
{type, [..., error: msg]}Static replacement message{:integer, gte: 18, error: "must be adult"}
{:required, type, [error: msg]}Static message on a required field{:required, :string, [error: "needed"]}
{:required, type, [error: {mod, fun, args}]}MFA receives %Peri.Error{} and returns string{:required, :string, [error: {Msgs, :email, []}]}
defmodule MyApp.Schemas do
  import Peri

  defmodule Msgs do
    def email_msg(%Peri.Error{content: ctx}), do: "email is invalid (#{inspect(ctx)})"
  end

  defschema :user, %{
    age:   {:integer, gte: 18, error: "must be adult"},
    email: {:required, :string, [error: {Msgs, :email_msg, []}]}
  }
end

For i18n, post-process the error list with Peri.Error.traverse_errors/2, which walks nested errors and replaces each leaf message with the callback's return value:

{:error, errors} = MyApp.Schemas.user(%{age: 10})

Peri.Error.traverse_errors(errors, fn err ->
  Gettext.dgettext(MyAppWeb.Gettext, "errors", err.message, err.content || %{})
end)

The MFA / static override fires first; traverse_errors/2 runs over whatever message remains (overridden or default). No hard dependency on Gettext — the callback is opaque.

Schema Transformation (Peri.walk/2)

Peri.walk/2 is a depth-first rewrite over a schema tree — a building block for derivations like "make every field optional" or "drop internal-only fields from a public DTO". The callback is invoked on every subtree, and its contract depends on context:

PositionCallback receivesValid returns
Map / keyword schema entry{:field, k, v}{:cont, {:field, k', v'}} or :drop
Any other subtree (type expression)the node itself{:cont, new_node} (:drop raises here)

Map keys, constraint option lists (e.g. [gte: 18, error: "..."]), :enum members, :literal values, :ref names, :multi tags, callbacks, and transforms are not visited — only sub-schemas and field entries are.

Make every field optional

Peri.walk(schema, fn
  {:required, t} -> {:cont, t}
  {:required, t, _opts} -> {:cont, t}
  other -> {:cont, other}
end)

Strip private fields from a DTO

Peri.walk(internal_schema, fn
  {:field, k, _v} when k in [:internal_id, :secret_token] -> :drop
  other -> {:cont, other}
end)

Rename a field

Peri.walk(schema, fn
  {:field, :email, v} -> {:cont, {:field, :login, v}}
  other -> {:cont, other}
end)

Compose with other tools

The result of walk/2 is a plain Peri schema, ready to feed to Peri.validate/2, Peri.to_json_schema/1, Peri.generate/1, or another walker pass.

Generator Overrides (gen: opt)

Constrained types like {:string, {:regex, …}} or {:integer, gt: 1_000_000} can be slow to generate via Peri.generate/1 because StreamData's default strategy is rejection sampling. Supply a gen: opt with a custom generator to skip rejection altogether:

FormDescription
gen: {Mod, :fun, args}MFA returning a %StreamData{}
gen: {Mod, :fun}MF (zero args), same return contract
gen: fn -> StreamData.… end0-arity function, same contract

Accepted positions:

%{
  # multi-options on a constrained primitive
  age:    {:integer, gte: 18, lte: 120, gen: {MyApp.Gens, :age, []}},

  # required wrapper
  login:  {:required, :string, [gen: {MyApp.Gens, :login}]},

  # meta wrapper (combines with docs/examples)
  email:  {:meta, {:required, {:string, {:regex, ~r/@/}}},
                  doc: "Login email",
                  gen: {MyApp.Gens, :email, []}}
}

Without an override, Peri.generate/1 falls back to chaining StreamData.filter/2 for each constraint.

Simple User Schema

%{
  name: {:required, :string},
  age: {:integer, {:gte, 0}},
  email: {:required, {:string, {:regex, ~r/@/}}},
  role: {:enum, [:admin, :user]}
}

Complex Nested Schema

%{
  user: %{
    profile: {:required, %{
      name: {:required, :string},
      bio: {:string, {:max, 500}}
    }},
    preferences: {:map, :string, :boolean},
    tags: {:list, :string}
  }
}