Z!

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.

Installation

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

Types

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.

Any

Module: Z.AnyShorthand::any

Rules

Note: The rules for Z.Any may be used for all other types as well since every type is implicitly a Z.Any

Atom

Module: Z.AtomShorthand::atom

Rules

Boolean

Module: Z.BooleanShorthand::boolean

Rules

Date

Module: Z.DateShorthand::date

Rules

DateTime

Module: Z.DateTimeShorthand::date_time

Rules

Float

Module: Z.FloatShorthand::float

Rules

Integer

Module: Z.IntegerShorthand::integer

Rules

List

Module: Z.ListShorthand::list

Rules

Map

Module: Z.MapShorthand::map

Rules

String

Module: Z.StringShorthand::string

Rules

Struct

Module: Z.Struct

Rules

Note: Don't use Z.Struct directly. Instead, define your own struct with use Z.Struct and a schema block

Time

Module: Z.TimeShorthand::time

Rules

Describing Schemas

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]

Validation

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"
 }}