View Source Usage

This page describes all keywords that are available for creating a schema. All schemas will construct in the "raw" format. The Xema.Builder provides some convenience functions to create schemas. The recommended method to construct a schema is use Xema described in the module documentation of Xema.

Type any

The schema any will accept any data.

iex> schema = Xema.new :any
iex> Xema.validate schema, 42
:ok
iex> Xema.validate schema, "foo"
:ok
iex> Xema.validate schema, nil
:ok

Type nil

The nil type matches only nil.

iex> schema = Xema.new :nil
iex> Xema.validate schema, nil
:ok
iex> {:error, error} = Xema.validate schema, 0
{:error, %Xema.ValidationError{
  reason: %{type: :nil, value: 0}
}}
iex> Exception.message(error)
"Expected nil, got 0."

Type boolean

The boolean type matches only true and false.

iex> schema = Xema.new :boolean
iex> Xema.validate schema, true
:ok
iex> Xema.valid? schema, false
true
iex> {:error, error} = Xema.validate schema, 0
{:error, %Xema.ValidationError{
  reason: %{type: :boolean, value: 0}
}}
iex> Exception.message(error)
"Expected :boolean, got 0."
iex> Xema.valid? schema, nil
false

Type atom

The atom type matches only atoms. Schemas of type atom match also the atoms true, false, and nil.

iex> schema = Xema.new :atom
iex> Xema.validate schema, :foo
:ok
iex> Xema.valid? schema, "foo"
false
iex> {:error, error} = Xema.validate schema, 0
{:error, %Xema.ValidationError{
  reason: %{type: :atom, value: 0}
}}
iex> Exception.message(error)
"Expected :atom, got 0."
iex> Xema.valid? schema, nil
true
iex> Xema.valid? schema, false
true

Type string

JSON Schema Draft: 4/6/7

The string type is used for strings.

iex> schema = Xema.new :string
iex> Xema.validate schema, "José"
:ok
iex> {:error, error} = Xema.validate schema, 42
{:error, %Xema.ValidationError{
  reason: %{type: :string, value: 42}
}}
iex> Exception.message(error)
"Expected :string, got 42."
iex> Xema.valid? schema, "José"
true
iex> Xema.valid? schema, 42
false

Length

The length of a string can be constrained using the min_length and max_length keywords. For both keywords, the value must be a non-negative number.

iex> schema = Xema.new {:string, min_length: 2, max_length: 3}
iex> {:error, error} = Xema.validate schema, "a"
{:error, %Xema.ValidationError{
  reason: %{value: "a", min_length: 2}
}}
iex> Exception.message(error)
~s|Expected minimum length of 2, got "a".|
iex> Xema.validate schema, "ab"
:ok
iex> Xema.validate schema, "abc"
:ok
iex> {:error, error} = Xema.validate schema, "abcd"
{:error, %Xema.ValidationError{
  reason: %{value: "abcd", max_length: 3}
}}
iex> Exception.message(error)
~s|Expected maximum length of 3, got "abcd".|

Regular Expression

The pattern keyword is used to restrict a string to a particular regular expression.

iex> schema = Xema.new {:string, pattern: ~r/[0-9]-[A-B]+/}
iex> Xema.validate schema, "1-AB"
:ok
iex> {:error, error} = Xema.validate schema, "foo"
{:error, %Xema.ValidationError{
  reason: %{value: "foo", pattern: ~r/[0-9]-[A-B]+/}
}}
iex> Exception.message(error)
~s|Pattern ~r/[0-9]-[A-B]+/ does not match value "foo".|

The regular expression can also be a string.

iex> schema = Xema.new {:string, pattern: "[0-9]-[A-B]+"}
iex> Xema.validate schema, "1-AB"
:ok
iex> {:error, error} = Xema.validate schema, "foo"
{:error, %Xema.ValidationError{
  reason: %{value: "foo", pattern: ~r/[0-9]-[A-B]+/}
}}
iex> Exception.message(error)
~s|Pattern ~r/[0-9]-[A-B]+/ does not match value "foo".|

Format

JSON Schema Draft: 4/6/7

Basic semantic validation of strings.

  • :date_time validation as defined by RFC 3339

    iex> schema = Xema.new {:string, format: :date_time}
    iex> Xema.valid? schema, "today"
    false
    iex> Xema.valid? schema, "1963-06-19T08:30:06.283185Z"
    true
  • :email validation as defined by RFC 5322

    iex> {:string, format: :email}
    ...> |> Xema.new()
    ...> |> Xema.valid?("marion.mustermann@otto.net")
    true
  • :host checks if the string is an valid IPv4, IPv6, or hostname.

  • :hostname validation as defined by RFC 1034

  • :ipv4 validation as defined by RFC 2673

  • :ipv6 validation as defined by RFC 2373

  • :uri validation as defindex by RFC 3986

    • :uri_fragment
    • :uri_path
    • :uri_query
    • :uri_userinfo

JSON Schema Draft: -/-/7

  • :regexchecks if the string is a valid regular expression.

Types number, integer and float

There are three numeric types in Xema: number, integer and float. They share the same validation keywords.

The number type is used for numbers.

iex> schema = Xema.new :number
iex> Xema.validate schema, 42
:ok
iex> Xema.validate schema, 21.5
:ok
iex> {:error, error} = Xema.validate schema, "foo"
{:error, %Xema.ValidationError{
  reason: %{type: :number, value: "foo"}
}}
iex> Exception.message(error)
~s|Expected :number, got "foo".|

The integer type is used for integral numbers.

iex> schema = Xema.new :integer
iex> Xema.validate schema, 42
:ok
iex> {:error, error} = Xema.validate schema, 21.5
{:error, %Xema.ValidationError{
  reason: %{type: :integer, value: 21.5}
}}
iex> Exception.message(error)
"Expected :integer, got 21.5."

The float type is used for floating point numbers.

iex> schema = Xema.new :float
iex> {:error, error} = Xema.validate schema, 42
{:error, %Xema.ValidationError{
  reason: %{type: :float, value: 42}
}}
iex> Exception.message(error)
"Expected :float, got 42."
iex> Xema.validate schema, 21.5
:ok

Multiples

JSON Schema Draft: 4/6/7

Numbers can be restricted to a multiple of a given number, using the multiple_of keyword. It may be set to any positive number.

iex> schema = Xema.new {:number, multiple_of: 2}
iex> Xema.validate schema, 8
:ok
iex> {:error, error} = Xema.validate schema, 7
{:error, %Xema.ValidationError{
  reason: %{value: 7, multiple_of: 2}
}}
iex> Exception.message(error)
"Value 7 is not a multiple of 2."
iex> Xema.valid? schema, 8.0
true

Range

JSON Schema Draft: 4/-/-

Ranges of numbers are specified using a combination of the minimum, maximum, exclusive_minimum and exclusive_maximum keywords.

  • minimum specifies a minimum numeric value.
  • exclusive_minimum is a boolean. When true, it indicates that the range excludes the minimum value, i.e., x > minx > min. When false (or not included), it indicates that the range includes the minimum value, i.e., x≥minx≥min.
  • maximum specifies a maximum numeric value.
  • exclusive_maximum is a boolean. When true, it indicates that the range excludes the maximum value, i.e., x < maxx < max. When false (or not included), it indicates that the range includes the maximum value, i.e., x ≤ maxx ≤ max.
iex> schema = Xema.new {
...>   :float,
...>   minimum: 1.2, maximum: 1.4, exclusive_maximum: true
...> }
iex> {:error, error} = Xema.validate schema, 1.1
{:error, %Xema.ValidationError{
  reason: %{value: 1.1, minimum: 1.2}
}}
iex> Exception.message(error)
"Value 1.1 is less than minimum value of 1.2."
iex> Xema.validate schema, 1.2
:ok
iex> Xema.valid? schema, 1.3
true
iex> {:error, error} = Xema.validate schema, 1.4
{:error, %Xema.ValidationError{
  reason: %{value: 1.4, maximum: 1.4, exclusive_maximum: true}
}}
iex> Exception.message(error)
"Value 1.4 equals exclusive maximum value of 1.4."
iex> {:error, error} = Xema.validate schema, 1.5
{:error, %Xema.ValidationError{
  reason: %{value: 1.5, maximum: 1.4, exclusive_maximum: true}
}}
iex> Exception.message(error)
"Value 1.5 exceeds maximum value of 1.4."

JSON Schema Draft: 6/7

The keywords exclusive_maximum and exclusive_minimum changed from a boolean to a number. Wherever one of these would be true before, they have now the value of the corresponding keyword maximum or minimum. The keyword maximum/minimum can be removed.

iex> schema = Xema.new {:float, minimum: 1.2, exclusive_maximum: 1.4}
iex> {:error, error} = Xema.validate schema, 1.1
{:error, %Xema.ValidationError{
  reason: %{value: 1.1, minimum: 1.2}
}}
iex> Exception.message(error)
"Value 1.1 is less than minimum value of 1.2."
iex> Xema.validate schema, 1.2
:ok
iex> Xema.valid? schema, 1.3
true
iex> {:error, error} = Xema.validate schema, 1.4
{:error, %Xema.ValidationError{
  reason: %{value: 1.4, exclusive_maximum: 1.4}
}}
iex> Exception.message(error)
"Value 1.4 equals exclusive maximum value of 1.4."
iex> {:error, error} = Xema.validate schema, 1.5
{:error, %Xema.ValidationError{
  reason: %{value: 1.5, exclusive_maximum: 1.4}
}}
iex> Exception.message(error)
"Value 1.5 exceeds maximum value of 1.4."

Type list

List are used for ordered elements, each element may be of a different type.

iex> schema = Xema.new :list
iex> Xema.valid? schema, [1, "two", 3.0]
true
iex> {:error, error} = Xema.validate schema, 9
{:error, %Xema.ValidationError{
  reason: %{type: :list, value: 9}
}}
iex> Exception.message(error)
"Expected :list, got 9."

Items

The items keyword will be used to validate all items of a list to a single schema.

iex> schema = Xema.new {:list, items: :string}
iex> Xema.valid? schema, ["a", "b", "abc"]
true
iex> {:error, error} = Xema.validate schema, ["a", 1]
{:error, %Xema.ValidationError{
  reason: %{items: %{1 => %{type: :string, value: 1}}}
}}
iex> Exception.message(error)
"Expected :string, got 1, at [1]."

The next example shows how to add keywords to the items schema.

iex> schema = Xema.new {:list, items: {:integer, minimum: 1, maximum: 10}}
iex> Xema.validate schema, [1, 2, 3]
:ok
iex> {:error, error} = Xema.validate schema, [3, 2, 1, 0]
{:error, %Xema.ValidationError{
  reason: %{items: %{3 => %{value: 0, minimum: 1}}}
}}
iex> Exception.message(error)
"Value 0 is less than minimum value of 1, at [3]."

items can also be used to give each item a specific schema.

iex> schema = Xema.new {
...>   :list,
...>   items: [:integer, {:string, min_length: 5}]
...> }
iex> Xema.valid? schema, [1, "hello"]
true
iex> {:error, error} = Xema.validate schema, [1, "five"]
{:error, %Xema.ValidationError{
  reason: %{items: %{1 => %{value: "five", min_length: 5}}}
}}
iex> Exception.message(error)
~s|Expected minimum length of 5, got "five", at [1].|
# It’s okay to not provide all of the items:
iex> Xema.validate schema, [1]
:ok
# And, by default, it’s also okay to add additional items to end:
iex> Xema.validate schema, [1, "hello", "foo"]
:ok

Additional Items

The additional_items keyword controls whether it is valid to have additional items in the array beyond what is defined in the schema.

iex> schema = Xema.new {
...>   :list,
...>   items: [:integer, {:string, min_length: 5}],
...>   additional_items: false
...> }
iex> Xema.validate schema, [1]
:ok
# It’s okay to not provide all of the items:
# But, since additionalItems is false, we can’t provide extra items:
iex> {:error, error} = Xema.validate schema, [1, "hello", "foo"]
{:error, %Xema.ValidationError{
  reason: %{items: %{2 => %{additional_items: false}}}
}}
iex> Exception.message(error)
"Unexpected additional item, at [2]."
iex> {:error, error} = Xema.validate schema, [1, "hello", "foo", "bar"]
{:error, %Xema.ValidationError{
  reason: %{
    items: %{
      2 => %{additional_items: false},
      3 => %{additional_items: false}
    }
  }
}}
iex> Exception.message(error)
"""
Unexpected additional item, at [2].
Unexpected additional item, at [3].\
"""

The keyword can also contain a schema to specify the type of additional items.

iex> schema = Xema.new {
...>   :list,
...>   items: [:integer, {:string, min_length: 3}],
...>   additional_items: :integer
...> }
iex> Xema.valid? schema, [1, "two", 3, 4]
true
iex> {:error, error} = Xema.validate schema, [1, "two", 3, "four"]
{:error, %Xema.ValidationError{
  reason: %{items: %{3 => %{type: :integer, value: "four"}}}
}}
iex> Exception.message(error)
~s|Expected :integer, got "four", at [3].|

Length

The length of the array can be specified using the min_items and max_items keywords. The value of each keyword must be a non-negative number.

iex> schema = Xema.new {:list, min_items: 2, max_items: 3}
iex> {:error, error} = Xema.validate schema, [1]
{:error, %Xema.ValidationError{
  reason: %{value: [1], min_items: 2}
}}
iex> Exception.message(error)
"Expected at least 2 items, got [1]."
iex> Xema.validate schema, [1, 2]
:ok
iex> Xema.validate schema, [1, 2, 3]
:ok
iex> {:error, error} = Xema.validate schema, [1, 2, 3, 4]
{:error, %Xema.ValidationError{
  reason: %{value: [1, 2, 3, 4], max_items: 3}
}}
iex> Exception.message(error)
"Expected at most 3 items, got [1, 2, 3, 4]."

Uniqueness

A schema can ensure that each of the items in an array is unique.

iex> schema = Xema.new {:list, unique_items: true}
iex> Xema.valid? schema, [1, 2, 3]
true
iex> {:error, error} = Xema.validate schema, [1, 2, 3, 2, 1]
{:error, %Xema.ValidationError{
  reason: %{value: [1, 2, 3, 2, 1], unique_items: true}
}}
iex> Exception.message(error)
"Expected unique items, got [1, 2, 3, 2, 1]."

Type tuple

Tuples are intended as fixed-size containers for multiple elements. The validation of tuples is similar to lists.

iex> schema = Xema.new {:tuple, min_items: 2, max_items: 3}
iex> {:error, error} = Xema.validate schema, {1}
{:error, %Xema.ValidationError{
  reason: %{value: {1}, min_items: 2}
}}
iex> Exception.message(error)
"Expected at least 2 items, got {1}."
iex> Xema.validate schema, {1, 2}
:ok
iex> Xema.validate schema, {1, 2, 3}
:ok
iex> {:error, error} = Xema.validate schema, {1, 2, 3, 4}
{:error, %Xema.ValidationError{
  reason: %{value: {1, 2, 3, 4}, max_items: 3}
}}
iex> Exception.message(error)
"Expected at most 3 items, got {1, 2, 3, 4}."

Type map

Whenever you need a key-value store, maps are the “go to” data structure in Elixir. Each of these pairs is conventionally referred to as a “property”.

iex> schema = Xema.new :map
iex> Xema.valid? schema, %{"foo" => "bar"}
true
iex> {:error, error} = Xema.validate schema, "bar"
{:error, %Xema.ValidationError{
  reason: %{type: :map, value: "bar"}
}}
iex> Exception.message(error)
~s|Expected :map, got "bar".|
# Using non-strings as keys are also valid:
iex> Xema.valid? schema, %{foo: "bar"}
true
iex> Xema.valid? schema, %{1 => "bar"}
true

Keys

The keyword keys can restrict the keys to atoms or strings.

Atoms as keys:

iex> schema = Xema.new {:map, keys: :atoms}
iex> Xema.valid? schema, %{"foo" => "bar"}
false
iex> Xema.valid? schema, %{foo: "bar"}
true
iex> Xema.valid? schema, %{1 => "bar"}
false

Strings as keys:

iex> schema = Xema.new {:map, keys: :strings}
iex> Xema.valid? schema, %{"foo" => "bar"}
true
iex> Xema.valid? schema, %{foo: "bar"}
false
iex> Xema.valid? schema, %{1 => "bar"}
false

Properties

The properties on a map are defined using the properties keyword. The value of properties is a map, where each key is the name of a property and each value is a schema used to validate that property.

iex> schema = Xema.new {:map,
...>   properties: %{
...>     a: :integer,
...>     b: {:string, min_length: 5}
...>   }
...> }
iex> Xema.valid? schema, %{a: 5, b: "hello"}
true
iex> {:error, error} = Xema.validate schema, %{a: 5, b: "ups"}
{:error, %Xema.ValidationError{
  reason: %{
    properties: %{
      b: %{value: "ups", min_length: 5}
    }
  }
}}
iex> Exception.message(error)
"Expected minimum length of 5, got \"ups\", at [:b]."
# Additinonal properties are allowed by default:
iex> Xema.valid? schema, %{a: 5, b: "hello", add: :prop}
true

Required Properties

By default, the properties defined by the properties keyword are not required. However, one can provide a list of required properties using the required keyword.

iex> schema = Xema.new {:map, properties: %{foo: :string}, required: [:foo]}
iex> Xema.validate schema, %{foo: "bar"}
:ok
iex> {:error, error} = Xema.validate schema, %{bar: "foo"}
{:error, %Xema.ValidationError{
  reason: %{required: [:foo]}
}}
iex> Exception.message(error)
"Required properties are missing: [:foo]."

Additional Properties

The additional_properties keyword is used to control the handling of extra stuff, that is, properties whose names are not listed in the properties keyword. By default any additional properties are allowed.

The additional_properties keyword may be either a boolean or an schema. If additional_properties is a boolean and set to false, no additional properties will be allowed.

iex> schema = Xema.new {:map,
...>   properties: %{foo: :string},
...>   required: [:foo],
...>   additional_properties: false
...> }
iex> Xema.validate schema, %{foo: "bar"}
:ok
iex> {:error, error} = Xema.validate schema, %{foo: "bar", bar: "foo"}
{:error, %Xema.ValidationError{
  reason: %{
    properties: %{bar: %{additional_properties: false}}
  }
}}
iex> Exception.message(error)
"Expected only defined properties, got key [:bar]."

additional_properties can also contain a schema to specify the type of additional properites.

iex> schema = Xema.new {
...>   :map,
...>   properties: %{foo: :string},
...>   additional_properties: :integer
...> }
iex> Xema.valid? schema, %{foo: "foo", add: 1}
true
iex> {:error, error} = Xema.validate schema, %{foo: "foo", add: "one"}
{:error, %Xema.ValidationError{
  reason: %{properties: %{add: %{type: :integer, value: "one"}}}
}}
iex> Exception.message(error)
~s|Expected :integer, got "one", at [:add].|

Pattern Properties

The keyword pattern_properties defined additional properties by regular expressions.

iex> schema = Xema.new {
...>   :map,
...>   additional_properties: false,
...>   pattern_properties: %{
...>     ~r/^s_/ => :string,
...>     ~r/^i_/ => :integer
...>   }
...> }
iex> Xema.valid? schema, %{"s_0" => "foo", "i_1" => 6}
true
iex> Xema.valid? schema, %{s_0: "foo", i_1: 6}
true
iex> {:error, error} = Xema.validate schema, %{s_0: "foo", f_1: 6.6}
{:error, %Xema.ValidationError{
  reason: %{properties: %{f_1: %{additional_properties: false}}}
}}
iex> Exception.message(error)
"Expected only defined properties, got key [:f_1]."

Size

The number of properties on an object can be restricted using the min_properties and max_properties keywords.

iex> schema = Xema.new {:map,
...>   min_properties: 2,
...>   max_properties: 3
...> }
iex> Xema.valid? schema, %{a: 1, b: 2}
true
iex> {:error, error} = Xema.validate schema, %{}
{:error, %Xema.ValidationError{
  reason: %{min_properties: 2, value: %{}}
}}
iex> Exception.message(error)
"Expected at least 2 properties, got %{}."
iex> {:error, error} = Xema.validate schema, %{a: 1, b: 2, c: 3, d: 4}
{:error, %Xema.ValidationError{
  reason: %{max_properties: 3, value: %{a: 1, b: 2, c: 3, d: 4}}
}}
iex> Exception.message(error)
"Expected at most 3 properties, got %{a: 1, b: 2, c: 3, d: 4}."

Size

The type of a key in the schema also matters in validation.

iex> schema = Xema.new {:map,
...>   properties: %{foo: :integer},
...>   additional_properties: false
...> }
iex> Xema.valid? schema, %{foo: 1}
true
iex> {:error, error} = Xema.validate schema, %{"foo" => 1}
{:error, %Xema.ValidationError{
  reason: %{properties: %{"foo" => %{additional_properties: false}}}
}}
iex> Exception.message(error)
~s|Expected only defined properties, got key [\"foo\"].|

Dependencies

The dependencies keyword allows the schema of the object to change based on the presence of certain special properties.

iex> schema = Xema.new {
...>   :map,
...>   properties: %{
...>     a: :number,
...>     b: :number,
...>     c: :number
...>   },
...>   dependencies: %{
...>     b: [:c]
...>   }
...> }
iex> Xema.valid? schema, %{a: 5}
true
iex> Xema.valid? schema, %{c: 9}
true
iex> Xema.valid? schema, %{b: 1}
false
iex> Xema.valid? schema, %{b: 1, c: 7}
true

Type struct

Structs can also be validated.

iex> schema = Xema.new :struct
iex> Xema.valid? schema, ~r/.*/
true
iex> Xema.valid? schema, %{}
false

Module

The module keyword allows specifying which struct is expected.

iex> schema = Xema.new {:struct, module: Regex}
iex> Xema.valid? schema, ~r/.*/
true
iex> Xema.valid? schema, URI.parse("")
false

Map keywords for structs

The validations for map are also available.

The next example shows a schema for an URI that needs a fragment.

iex> schema = Xema.new {
...>   :struct, module: URI, properties: %{fragment: :string}
...> }
iex>
iex> Xema.valid?(schema, URI.parse("http://example.com"))
false
iex> Xema.valid?(schema, URI.parse("http://example.com#frag"))
true

Multiples Types

JSON Schema Draft: 4/6/7

It is also possible to check if a value matches one of several types.

iex> schema = Xema.new {[:string, nil], min_length: 1}
iex> Xema.valid? schema, "foo"
true
iex> Xema.valid? schema, nil
true
iex> Xema.valid? schema, ""
false

Allow Additional Types

JSON Schema Draft: -

The keyword allow adds an extra type to the schema validation.

iex> schema = Xema.new {:string, min_length: 1, allow: nil}
iex> Xema.valid? schema, "foo"
true
iex> Xema.valid? schema, nil
true
iex> Xema.valid? schema, ""
false

Constants

JSON Schema Draft: -/6/7

This keyword checks if a value is equals to the given const.

iex> schema = Xema.new(const: 4711)
iex> Xema.validate schema, 4711
:ok
iex> {:error, error} = Xema.validate schema, 333
{:error, %Xema.ValidationError{
  reason: %{const: 4711, value: 333}
}}
iex> Exception.message(error)
"Expected 4711, got 333."

Enumerations

The enum keyword is used to restrict a value to a fixed set of values. It must be an array with at least one element, where each element is unique.

iex> schema = Xema.new {:any, enum: [1, "foo", :bar]}
iex> Xema.valid? schema, :bar
true
iex> Xema.valid? schema, 42
false

Negate Schema

The keyword not negates a schema.

iex> schema = Xema.new(not: {:integer, minimum: 0})
iex> Xema.valid? schema, 10
false
iex> Xema.valid? schema, -10
true

If-Then-Else

The keywords if, then, else work together to implement conditional application of a subschema based on the outcome of another subschema.

iex> schema =
...>   Xema.new(
...>     if: :list,
...>     then: [items: :integer, min_items: 2],
...>     else: :integer
...>   )
...>
...> Xema.valid?(schema, 3)
true
iex> Xema.valid?(schema, "3")
false
iex> Xema.valid?(schema, [1])
false
iex> Xema.valid?(schema, [1, 2])
true

Custom validator

With the keyword validator a custom validator can be defined. The validator expected a function or a tuple of module and function name. The validator function gets the current value and return :ok on success and an error tuple on failure.

iex> defmodule Palindrome do
...>   @xema Xema.new(
...>     properties: %{
...>       palindrome: {:string, validator: &Palindrome.check/1}
...>     }
...>   )
...>
...>   def check(value) do
...>     case value == String.reverse(value) do
...>       true -> :ok
...>       false -> {:error, :no_palindrome}
...>     end
...>   end
...>
...>   def validate(value) do
...>     Xema.validate(@xema, value)
...>   end
...> end
iex>
iex> Palindrome.validate(%{palindrome: "abba"})
:ok
iex> {:error, error} = Palindrome.validate(%{palindrome: "beatles"})
{:error, %Xema.ValidationError{
  reason: %{
    properties: %{
      palindrome: %{validator: :no_palindrome, value: "beatles"}
    }
  }
}}
iex> Exception.message(error)
~s|Validator fails with :no_palindrome for value "beatles", at [:palindrome].|

Combine Schemas

The keywords all_of, any_of, and one_of combines schemas.

With all_of all schemas have to match.

iex> all = Xema.new(all_of: [
...>   {:integer, multiple_of: 2},
...>   {:integer, multiple_of: 3}
...> ])
iex> 0..9 |> Enum.map(&Xema.valid?(all, &1)) |> Enum.with_index()
[true: 0, false: 1, false: 2, false: 3, false: 4,
 false: 5, true: 6, false: 7, false: 8, false: 9]

With any_of any schema have to match.

iex> any = Xema.new(any_of: [
...>   {:integer, multiple_of: 2},
...>   {:integer, multiple_of: 3}
...> ])
iex> 0..9 |> Enum.map(&Xema.valid?(any, &1)) |> Enum.with_index()
[true: 0, false: 1, true: 2, true: 3, true: 4,
 false: 5, true: 6, false: 7, true: 8, true: 9]

With one_of exactly on schema have to match.

iex> one = Xema.new(one_of: [
...>   {:integer, multiple_of: 2},
...>   {:integer, multiple_of: 3}
...> ])
iex> 0..9 |> Enum.map(&Xema.valid?(one, &1)) |> Enum.with_index()
[false: 0, false: 1, true: 2, true: 3, true: 4,
 false: 5, false: 6, false: 7, true: 8, true: 9]

Structuring a schema

This section will present some options to structuring and reuse schemas.

definitions and ref

To reuse a schema put it under the keyword definitions. Later on, the schema can be referenced with the keyword ref.

iex> schema = Xema.new {
...>   :map,
...>   definitions: %{
...>     positive: {:integer, minimum: 1},
...>     negative: {:integer, maximum: -1}
...>   },
...>   properties: %{
...>     a: {:ref, "#/definitions/positive"},
...>     b: {:ref, "#/definitions/positive"},
...>     c: {:ref, "#/definitions/negative"},
...>     # You can also reference any other schema.
...>     d: {:ref, "#/properties/c"}
...>   }
...> }
...> Xema.validate schema, %{a: 1, c: -1}
:ok
iex> {:error, error} = Xema.validate schema, %{b: 1, c: 1}
{:error, %Xema.ValidationError{
  reason: %{properties: %{c: %{maximum: -1, value: 1}}}
}}
iex> Exception.message(error)
"Value 1 exceeds maximum value of -1, at [:c]."
iex> Xema.validate schema, %{d: -1}
:ok
iex> {:error, error} = Xema.validate schema, %{d: 1}
{:error, %Xema.ValidationError{
  reason: %{properties: %{d: %{maximum: -1, value: 1}}}
}}
iex> Exception.message(error)
"Value 1 exceeds maximum value of -1, at [:d]."

Without definitions and ref

It is also possible to use a schema in another schema, as in the following code.

iex> positive = Xema.new {:integer, minimum: 1}
...> negative = Xema.new {:integer, maximum: -1}
...> schema = Xema.new {
...>   :map,
...>   properties: %{
...>     a: positive,
...>     b: positive,
...>     c: negative
...>   }
...> }
...> Xema.validate schema, %{a: 1, b: 2, c: -3}
:ok

In multiple files

To structure schemas in multiple files you have to configure a loader to load the files. The section "Configure a loader" described the configuration and implementation of an loader.

Let's assume you have the following file available at https://localhost:1234/positive.exon.

{
  :string,
  definitions: %{
    or_nil: {:any_of, [:nil, {:ref, "#"}]}
  }
}

I admit this example is a little absurd. But the URL could be also any other URL and with another loader, the file could be on your hard disk.

You can use the schema above as follow.

iex> alias Test.RemoteLoaderExon
iex> schema = Xema.new({
...>   :map,
...>   id: "http://localhost:1234",
...>   properties: %{
...>     name: {:ref, "xema_name.exon#/definitions/or_nil"},
...>     str: {:ref, "xema_name.exon"}
...>   }
...> }, loader: RemoteLoaderExon)
iex> Xema.validate schema, %{str: "foo"}
:ok
iex> {:error, error} = Xema.validate schema, %{str: nil}
{:error, %Xema.ValidationError{
  reason: %{properties: %{str: %{type: :string, value: nil}}}
}}
iex> Exception.message(error)
"Expected :string, got nil, at [:str]."
iex> Xema.validate schema, %{name: nil}
:ok
iex> Xema.validate schema, %{name: "Funny van Dannen"}
:ok
iex> {:error, error} = Xema.validate schema, %{name: 66}
{:error,
  %Xema.ValidationError{
    reason: %{
      properties: %{
        name: %{any_of: [
          %{type: nil, value: 66},
          %{type: :string, value: 66}
        ],
        value: 66}
      }
    }
  }
}
iex> Exception.message(error)
"""
No match of any schema, at [:name].
  Expected nil, got 66, at [:name].
  Expected :string, got 66, at [:name].\
"""