Xema
Xema is a schema validator inspired by JSON Schema.
Xema allows you to annotate and validate elixir data structures.
Xema is in early beta. If you try it and has an issue, report them.
Installation
First, add Xema to your mix.exs
dependencies:
def deps do
[{:xema, "~> 0.1"}]
end
Then, update your dependencies:
$ mix deps.get
Usage
Xema supported the following types to validate data structures.
Type any
The schema any will accept any data.
iex> import Xema
Xema
iex> schema = xema :any
%Xema{type: %Xema.Any{}}
iex> validate schema, 42
:ok
iex> validate schema, "foo"
:ok
iex> validate schema, nil
:ok
Type nil
The nil type matches only nil
.
iex> import Xema
Xema
iex> schema = xema :nil
%Xema{type: %Xema.Nil{}}
iex> validate schema, nil
:ok
iex> validate schema, 0
{:error, %{reason: :wrong_type, type: :nil}}
Type boolean
The boolean type matches only true
and false
.
iex> import Xema
Xema
iex> schema = xema :boolean
%Xema{type: %Xema.Boolean{}}
iex> validate schema, true
:ok
iex> is_valid? schema, false
true
iex> validate schema, 0
{:error, %{reason: :wrong_type, type: :boolean}}
iex> is_valid? schema, nil
false
Type string
The string type is used for strings.
iex> import Xema
Xema
iex> schema = xema :string
%Xema{type: %Xema.String{}}
iex> validate schema, "José"
:ok
iex> validate schema, 42
{:error, %{reason: :wrong_type, type: :string}}
iex> is_valid? schema, "José"
true
iex> is_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> import Xema
Xema
iex> schema = xema :string, min_length: 2, max_length: 3
%Xema{type: %Xema.String{min_length: 2, max_length: 3}}
iex> validate schema, "a"
{:error, %{reason: :too_short, min_length: 2}}
iex> validate schema, "ab"
:ok
iex> validate schema, "abc"
:ok
iex> validate schema, "abcd"
{:error, %{reason: :too_long, max_length: 3}}
Regular Expression
The pattern
keyword is used to restrict a string to a particular regular expression.
iex> import Xema
Xema
iex> schema = xema :string, pattern: ~r/[0-9]-[A-B]+/
%Xema{type: %Xema.String{pattern: ~r/[0-9]-[A-B]+/}}
iex> validate schema, "1-AB"
:ok
iex> validate schema, "foo"
{:error, %{reason: :no_match, pattern: ~r/[0-9]-[A-B]+/}}
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> import Xema
Xema
iex> schema = xema :number
%Xema{type: %Xema.Number{}}
iex> validate schema, 42
:ok
iex> validate schema, 21.5
:ok
iex> validate schema, "foo"
{:error, %{reason: :wrong_type, type: :number}}
The integer
type is used for integral numbers.
iex> import Xema
Xema
iex> schema = xema :integer
%Xema{type: %Xema.Integer{}}
iex> validate schema, 42
:ok
iex> validate schema, 21.5
{:error, %{reason: :wrong_type, type: :integer}}
The float
type is used for floating point numbers.
iex> import Xema
Xema
iex> schema = xema :float
%Xema{type: %Xema.Float{}}
iex> validate schema, 42
{:error, %{reason: :wrong_type, type: :float}}
iex> validate schema, 21.5
:ok
Multiples
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> import Xema
Xema
iex> schema = xema :number, multiple_of: 2
%Xema{type: %Xema.Number{multiple_of: 2}}
iex> validate schema, 8
:ok
iex> validate schema, 7
{:error, %{reason: :not_multiple, multiple_of: 2}}
iex> is_valid? schema, 8.0
true
Range
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> import Xema
Xema
iex> schema = xema :float, minimum: 1.2, maximum: 1.4, exclusive_maximum: true
%Xema{type: %Xema.Float{minimum: 1.2, maximum: 1.4, exclusive_maximum: true}}
iex> validate schema, 1.1
{:error, %{reason: :too_small, minimum: 1.2}}
iex> validate schema, 1.2
:ok
iex> is_valid? schema, 1.3
true
iex> validate schema, 1.4
{:error, %{reason: :too_big, maximum: 1.4, exclusive_maximum: true}}
iex> validate schema, 1.5
{:error, %{reason: :too_big, maximum: 1.4}}
Type list
List are used for ordered elements, each element may be of a different type.
iex> import Xema
Xema
iex> schema = xema :list
%Xema{type: %Xema.List{}}
iex> is_valid? schema, [1, "two", 3.0]
true
iex> validate schema, 9
{:error, %{reason: :wrong_type, type: :list}}
Items
The items
keyword will be used to validate all items of a list to a single
schema.
iex> import Xema
Xema
iex> schema = xema :list, items: :string
%Xema{type: %Xema.List{items: %Xema.String{}}}
iex> is_valid? schema, ["a", "b", "abc"]
true
iex> validate schema, ["a", 1]
{
:error,
%{reason: :invalid_item, at: 1, error: %{reason: :wrong_type, type: :string}}
}
The next example shows how to add keywords to the items schema.
iex> import Xema
Xema
iex> schema = xema :list, items: {:integer, minimum: 1, maximum: 10}
%Xema{type: %Xema.List{items: %Xema.Integer{minimum: 1, maximum: 10}}}
iex> validate schema, [1, 2, 3]
:ok
iex> validate schema, [3, 2, 1, 0]
{
:error,
%{reason: :invalid_item, at: 3, error: %{reason: :too_small, minimum: 1}}
}
items
can also be used to give each item a specific schema.
iex> import Xema
Xema
iex> schema = xema :list,
...> items: [:integer, {:string, min_length: 5}]
%Xema{type: %Xema.List{
items: [%Xema.Integer{}, %Xema.String{min_length: 5}]
}}
iex> is_valid? schema, [1, "hello"]
true
iex> validate schema, [1, "five"]
{
:error,
%{reason: :invalid_item, at: 1, error: %{reason: :too_short, min_length: 5}}
}
# It’s okay to not provide all of the items:
iex> validate schema, [1]
:ok
# And, by default, it’s also okay to add additional items to end:
iex> 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> import Xema
Xema
iex> schema = xema :list,
...> items: [:integer, {:string, min_length: 5}],
...> additional_items: false
%Xema{type: %Xema.List{
items: [%Xema.Integer{}, %Xema.String{min_length: 5}],
additional_items: false
}}
# It’s okay to not provide all of the items:
iex> validate schema, [1]
:ok
# But, since additionalItems is false, we can’t provide extra items:
iex> validate schema, [1, "hello", "foo"]
{:error, %{reason: :additional_item, at: 2}}
iex> validate schema, [1, "hello", "foo", "bar"]
{:error, %{reason: :additional_item, at: 2}}
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> import Xema
Xema
iex> schema = xema :list, min_items: 2, max_items: 3
%Xema{type: %Xema.List{min_items: 2, max_items: 3}}
iex> validate schema, [1]
{:error, %{reason: :too_less_items, min_items: 2}}
iex> validate schema, [1, 2]
:ok
iex> validate schema, [1, 2, 3]
:ok
iex> validate schema, [1, 2, 3, 4]
{:error, %{reason: :too_many_items, max_items: 3}}
Uniqueness
A schema can ensure that each of the items in an array is unique.
iex> import Xema
Xema
iex> schema = xema :list, unique_items: true
%Xema{type: %Xema.List{unique_items: true}}
iex> is_valid? schema, [1, 2, 3]
true
iex> validate schema, [1, 2, 3, 2, 1]
{:error, %{reason: :not_unique}}
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> import Xema
Xema
iex> schema = xema :map
%Xema{type: %Xema.Map{}}
iex> is_valid? schema, %{"foo" => "bar"}
true
iex> validate schema, "bar"
{:error, %{reason: :wrong_type, type: :map}}
# Using non-strings as keys are also valid:
iex> is_valid? schema, %{foo: "bar"}
true
iex> is_valid? schema, %{1 => "bar"}
true
Keys
The keyword keys
can restrict the keys to atoms or strings.
Atoms as keys:
iex> import Xema
Xema
iex> schema = xema :map, keys: :atoms
%Xema{type: %Xema.Map{keys: :atoms}}
iex> is_valid? schema, %{"foo" => "bar"}
false
iex> is_valid? schema, %{foo: "bar"}
true
iex> is_valid? schema, %{1 => "bar"}
false
Strings as keys:
iex> import Xema
Xema
iex> schema = xema :map, keys: :strings
%Xema{type: %Xema.Map{keys: :strings}}
iex> is_valid? schema, %{"foo" => "bar"}
true
iex> is_valid? schema, %{foo: "bar"}
false
iex> is_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> import Xema
Xema
iex> schema = xema :map,
...> properties: %{
...> a: :integer,
...> b: {:string, min_length: 5}
...> }
%Xema{type: %Xema.Map{
properties: %{
a: %Xema.Integer{},
b: %Xema.String{min_length: 5}
}
}}
iex> is_valid? schema, %{a: 5, b: "hello"}
true
iex> validate schema, %{a: 5, b: "ups"}
{:error, %{
reason: :invalid_property,
property: :b,
error: %{
reason: :too_short,
min_length: 5
}
}}
# Additinonal properties are allowed by default:
iex> is_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> import Xema
Xema
iex> schema = xema :map, properties: %{foo: :string}, required: [:foo]
%Xema{
type: %Xema.Map{
properties: %{foo: %Xema.String{}},
required: MapSet.new([:foo])
}
}
iex> validate schema, %{foo: "bar"}
:ok
iex> validate schema, %{bar: "foo"}
{:error, %{reason: :missing_properties, missing: [:foo], required: [: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 object. If
additional_properties
is a boolean and set to false, no additional properties
will be allowed.
iex> import Xema
Xema
iex> schema = xema :map,
...> properties: %{foo: :string},
...> required: [:foo],
...> additional_properties: false
%Xema{
type: %Xema.Map{
properties: %{foo: %Xema.String{}},
required: MapSet.new([:foo]),
additional_properties: false
}
}
iex> validate schema, %{foo: "bar"}
:ok
iex> validate schema, %{foo: "bar", bar: "foo"}
{:error, %{
reason: :no_additional_properties_allowed,
additional_properties: [:bar]}
}
Pattern Properties
The keyword pattern_properties
defined additional properties by regular
expressions.
iex> import Xema
Xema
iex> schema = xema :map,
...> additional_properties: false,
...> pattern_properties: %{
...> ~r/^s_/ => :string,
...> ~r/^i_/ => :integer
...> }
%Xema{type: %Xema.Map{
additional_properties: false,
pattern_properties: %{
~r/^s_/ => %Xema.String{},
~r/^i_/ => %Xema.Integer{}
}
}}
iex> is_valid? schema, %{"s_0" => "foo", "i_1" => 6}
true
iex> is_valid? schema, %{s_0: "foo", i_1: 6}
true
iex> validate schema, %{s_0: "foo", f_1: 6.6}
{:error, %{
reason: :no_additional_properties_allowed,
additional_properties: [:f_1]
}}
Size
The number of properties on an object can be restricted using the
min_properties
and max_properties
keywords.
iex> import Xema
Xema
iex> schema = xema :map,
...> min_properties: 2,
...> max_properties: 3
%Xema{type: %Xema.Map{
min_properties: 2,
max_properties: 3
}}
iex> is_valid? schema, %{a: 1, b: 2}
true
iex> validate schema, %{}
{:error, %{reason: :too_less_properties, min_properties: 2}}
iex> validate schema, %{a: 1, b: 2, c: 3, d: 4}
{:error, %{reason: :too_many_properties, max_properties: 3}}
Dependencies
The dependencies
keyword allows the schema of the object to change based on
the presence of certain special properties.
iex> import Xema
Xema
iex> schema = xema :map,
...> properties: %{
...> a: :number,
...> b: :number,
...> c: :number
...> },
...> dependencies: %{
...> b: [:c]
...> }
%Xema{type: %Xema.Map{
properties: %{a: %Xema.Number{}, b: %Xema.Number{}, c: %Xema.Number{}},
dependencies: %{b: [:c]}
}}
iex> is_valid? schema, %{a: 5}
true
iex> is_valid? schema, %{c: 9}
true
iex> is_valid? schema, %{b: 1}
false
iex> is_valid? schema, %{b: 1, c: 7}
true
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> import Xema
Xema
iex> schema = xema :any, enum: [1, "foo", :bar]
%Xema{type: %Xema.Any{enum: [1, "foo", :bar]}}
iex> is_valid? schema, :bar
true
iex> is_valid? schema, 42
false
References
The home of JSON Schema: http://json-schema.org/
Specification:
- JSON Schema core defines the basic foundation of JSON Schema
- JSON Schema Validation defines the validation keywords of JSON Schema
Understanding JSON Schema a great tutorial for JSON Schema authors and a template for the description of Xema.