Zoi
is a schema validation library for Elixir, designed to provide a simple and flexible way to define and validate data.
zoi
to your list of dependencies in mix.exs
:
def deps do
[
{:zoi, "~> 0.6"}
]
end
You can create schemas for various data types, including strings, integers, floats, booleans, arrays, maps, and more. Zoi
supports a wide range of validation rules and transformations.
Here's a simple example of how to use Zoi
to validate a string:
# Define a schema with a string type
iex> schema = Zoi.string() |> Zoi.min(3)
iex> Zoi.parse(schema, "hello")
{:ok, "hello"}
iex> Zoi.parse(schema, "hi")
{:error, [%Zoi.Error{message: "too small: must have at least 3 characters"}]}
# Add transforms to a schema
iex> schema = Zoi.string() |> Zoi.trim()
iex> Zoi.parse(schema, " world ")
{:ok, "world"}
You can also validate structured maps:
# Validate a structured data in a map
iex> schema = Zoi.object(%{name: Zoi.string(), age: Zoi.integer(), email: Zoi.email()})
iex> Zoi.parse(schema, %{name: "John", age: 30, email: "john@email.com"})
{:ok, %{name: "John", age: 30, email: "john@email.com"}}
iex> Zoi.parse(schema, %{email: "invalid-email"})
{:error, [
%Zoi.Error{path: [:name], message: "is required"},
%Zoi.Error{path: [:age], message: "is required"},
%Zoi.Error{path: [:email], message: "invalid email format"}
]}
and arrays:
# Validate an array of integers
iex> schema = Zoi.array(Zoi.integer() |> Zoi.min(0)) |> Zoi.min(2)
iex> Zoi.parse(schema, [1, 2, 3])
{:ok, [1, 2, 3]}
iex> Zoi.parse(schema, [1, "2"])
{:error, [%Zoi.Error{path: [1], message: "invalid type: must be an integer"}]}
And many more possibilities, including nested schemas, custom validations and data transformations. Check the official docs for more details.
Zoi
can infer types from schemas, allowing you to leverage Elixir's @type
and @spec
annotations for documentation
defmodule MyApp.Schema do
@schema Zoi.string() |> Zoi.min(2) |> Zoi.max(100)
@type t :: unquote(Zoi.type_spec(@schema))
end
This will generate the following type specification:
@type t :: binary()
This also applies to complex types, such as Zoi.object/2
:
defmodule MyApp.User do
@schema Zoi.object(%{
name: Zoi.string() |> Zoi.min(2) |> Zoi.max(100),
age: Zoi.integer() |> Zoi.optional(),
email: Zoi.email()
})
@type t :: unquote(Zoi.type_spec(@schema))
end
Which will generate:
@type t :: %{
required(:name) => binary(),
optional(:age) => integer(),
required(:email) => binary()
}
When validation fails, Zoi
returns a list of errors, each containing a message and the path to the invalid data. Even when erros are nested, Zoi
will return all errors in a flattened list.
iex> schema = Zoi.object(%{name: Zoi.string(), age: Zoi.integer()})
iex> Zoi.parse(schema, %{name: 123, age: "thirty"})
{:error, [
%Zoi.Error{path: [:name], message: "invalid type: must be a string"},
%Zoi.Error{path: [:age], message: "invalid type: must be an integer"}
]}
You can view the error in a map format using the Zoi.treefy_errors/1
function:
iex> Zoi.treefy_errors(errors)
%{
name: ["invalid type: must be a string"],
age: ["invalid type: must be an integer"]
}
You can also customize error messages:
iex> schema = Zoi.string(error: "not a string")
iex> Zoi.parse(schema, :hi)
{:error, [%Zoi.Error{message: "not a string"}]}
You can attach metadata to schemas using the :metadata
option. This metadata can be useful for documentation, testing, or other any purpose your application may require.
iex> schema = Zoi.string(metadata: [id: "1", description: "A simple string"])
iex> Zoi.metadata(schema)
[id: "1", description: "A simple string"]
You can use this feature to create self-documenting schemas, with example and tests. For example:
defmodule MyApp.UserSchema do
@schema Zoi.object(
%{
name: Zoi.string() |> Zoi.min(2) |> Zoi.max(100),
age: Zoi.integer() |> Zoi.optional()
},
metadata: [
example: %{name: "Alice", age: 30},
doc: "A user schema with name and optional age",
moduledoc: "Schema representing a user with name and optional age"
]
)
@moduledoc """
#{Zoi.metadata(@schema)[:moduledoc]}
"""
@doc """
#{Zoi.metadata(@schema)[:doc]}
"""
def schema, do: @schema
end
defmodule MyApp.UserSchemaTest do
use ExUnit.Case
alias MyApp.UserSchema
test "example matches schema" do
example = Zoi.metadata(UserSchema.schema())[:example]
assert {:ok, example} == Zoi.parse(UserSchema.schema(), example)
end
end
Zoi
is inspired by Zod and Joi, providing a similar experience for Elixir.