Z! is a schema description and data validation library. Inspired by libraries like Joi, Yup, and Zod from the JavaScript community, Z! helps you describe schemas for your structs and validate their data at runtime.
This package can be installed by adding zbang
to your list of dependencies in mix.exs
:
def deps do
[
{:zbang, "~> 1.1.1"}
]
end
The docs can be found at https://hexdocs.pm/zbang
Many types can be validated with Z!. Below is a list of built-in primitive types, but you can also define custom types of your own.
Module: Z.Any
Shorthand: :any
Rules
:default
- If the input isnil
, sets input to given value- You may set the default to any literal value. When defined on a struct field, the value will be validated against the field type and all other field rules at compile time.
- You may also set the default to any function with an arity of zero. The function will be evaluated at validation time and the returned value will be used as the default value. When defined on a struct field, the function must be a named function and will not be evaluated or validated at compile time.
:required
- Asserts that input is notnil
:equals
- Asserts that input is equal to given value:enum
- Asserts that input is in list of given values
Note: The rules for
Z.Any
may be used for all other types as well since every type is implicitly aZ.Any
Module: Z.Atom
Shorthand: :atom
Rules
:parse
- If input is a string, try to parse it to an atom
Module: Z.Boolean
Shorthand: :boolean
Rules
:parse
- if input is a string, try to parse it to a boolean
Module: Z.Date
Shorthand: :date
Rules
:parse
- If input is a string, try to parse it to a Date:trunc
- If input is a DateTime or NaiveDateTime, convert it to a Date:min
- Asserts that the input is at least the given Date or after:max
- Asserts that the input is at most the given Date or before
Module: Z.DateTime
Shorthand: :date_time
Rules
:parse
- If input is a string, try to parse it to a DateTime:allow_int
- If input is an integer, try to convert it to a DateTime:shift
- Shift the input to the same point in time at the given timezone:trunc
- Truncates the microsecond field of the input to the given precision:min
- Asserts that the input is at least the given DateTime or after:max
- Asserts that the input is at most the given DateTime or before
Module: Z.Float
Shorthand: :float
Rules
:parse
- If input is a string, try to parse it to a float:allow_int
- If input is an integer, convert it to a float:min
- Asserts that input is greater than or equal to given value:max
- Asserts that input is less than or equal to given value:greater_than
- Asserts that input is greater than given value:less_than
- Asserts that input is less than given value
Module: Z.Integer
Shorthand: :integer
Rules
:parse
- If input is a string, try to parse it to an integer:trunc
- If input is a float, truncate it to an integer:min
- Asserts that input is greater than or equal to given value:max
- Asserts that input is less than or equal to given value:greater_than
- Asserts that input is greater than given value:less_than
- Asserts that input is less than given value
Module: Z.List
Shorthand: :list
Rules
:items
- Validates the items in the input list:length
- Asserts that input length is equal to the given value:min
- Asserts that input length is at least the given length:max
- Asserts that input length is at most the given length
Module: Z.Map
Shorthand: :map
Rules
:atomize_keys
- If key is a string, try to parse it to an atom (only existing atoms by default):size
- Asserts that the input size is equal to the given value:min
- Asserts that the input size is at least the given value:max
- Asserts that the input size is at most the given value
Module: Z.String
Shorthand: :string
Rules
:trim
- Trims any leading or trailing whitespace from the input:length
- Asserts that input length is equal to the given value:min
- Asserts that input length is at least the given length:max
- Asserts that input length is at most the given length
Module: Z.Struct
Rules
:cast
- If the input is a Map, try to cast it to the given struct
Note: Don't use
Z.Struct
directly. Instead, define your own struct withuse Z.Struct
and aschema
block
Module: Z.Time
Shorthand: :time
Rules
:parse
- If input is a string, try to parse it to a Time:trunc
- Truncates the microsecond field of the input to the given precision:min
- Asserts that the input is at least the given Time or after:max
- Asserts that the input is at most the given Time or before
Example
defmodule Money do
use Z.Struct
schema do
field :amount, :float, [:required, :parse, min: 0.0]
field :currency, :string, [:required, default: "USD", enum: ["USD", "EUR", "BTC"]]
end
end
defmodule Book do
use Z.Struct
schema do
field :title, :string, [:required]
field :author, :string, [:required, default: "Unknown"]
field :description, :string
field :price, Money, [:required, :cast]
field :read_at, :datetime, [default: &DateTime.utc_now/0]
end
end
In the above example, we are defining two structs by employing use Z.Struct
with a schema
block where fields
are defined. When you define a struct in this way, Z.Struct
will call defstruct
for you and create an Elixir struct with defaults when given. In addition, it will define a validate/3
function on your struct module that can be used to validate values at runtime.
The validate/3
function uses the fields defined in the schema
block to automatically assert the type of each value as well as assert that the given rules are being followed.
In addition to the validate/3
function, new/1
and new!/1
functions are also added for instantiating your structs from a keyword list or any other key-value enumerable. These functions will also validate your newly created struct.
Each field
takes a name
, type
and optional rules
. The name
must be an atom. The type
must also be an atom and can either be a built-in type or a custom type e.g. the Money
type used by the :price
field in the example above. The rules
vary depending on the type
given. See here for a list of all rules per type.
All :required
fields will be added to @enforce_keys
by default. If you don't want to enforce a required field at compile time, you may opt out of this behavior with required: [enforce: false]
Validating data is as simple as calling validate/3
on the type that you would like to assert and passing in optional rules. The validate/3
function will return either {:ok, value}
or {:error, error}
.
Alternatively, you can use validate!/3
which has the same signature as validate/3
but returns the validated value
instead of the {:ok, value}
tuple. If there are issues during validation, validate!/3
will raise a Z.Error
Examples
Z.String.validate("hello world")
{:ok, "hello world"}
Z.String.validate("oops", length: 5)
{:error,
%Z.Error{
issues: [
%Z.Issue{
code: "too_small",
message: "input does not have correct length",
path: ["."]
}
],
message: "invalid"
}}
Z.String.validate(nil, [:required, default: "sleepy bear"])
{:ok, "sleepy bear"}
Book.validate(%{title: "I <3 Elixir", price: %{amount: "1.00"}})
{:error,
%Z.Error{
issues: [
%Z.Issue{code: "invalid_type", message: "input is not a Book", path: ["."]}
],
message: "invalid"
}}
Book.validate(%{title: "I <3 Elixir", price: %{amount: "1.00"}}, [:cast])
{:ok,
%Book{
author: "Unknown",
description: nil,
price: %Money{amount: 1.0, currency: "USD"},
read_at: ~U[2022-07-19 04:14:58.979221Z],
title: "I <3 Elixir"
}}