Zoi
View Source
Zoi is a schema validation library for Elixir, designed to provide a simple and flexible way to define and validate data.
Installation
zoi to your list of dependencies in mix.exs:
def deps do
[
{:zoi, "~> 0.10"}
]
endUsage
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.
Parsing Data
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{
code: :greater_than_or_equal_to,
issue: {"too small: must have at least %{count} character(s)", [count: 3]},
message: "too small: must have at least 3 character(s)",
path: []
}
]}
# 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> {:error, errors} = Zoi.parse(schema, %{email: "invalid-email"})
iex> Zoi.treefy_errors(errors)
%{name: ["is required"], email: ["invalid email format"], age: ["is required"]}or 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{
code: :invalid_type,
issue: {"invalid type: expected integer", [type: :integer]},
message: "invalid type: expected integer",
path: [1]
}
]}keywords:
# Validate a keyword list
iex> schema = Zoi.keyword(email: Zoi.email(), allow?: Zoi.boolean())
iex> Zoi.parse(schema, [email: "john@email.com", allow?: true])
{:ok, [email: "john@email.com", allow?: true]}
iex> Zoi.parse(schema, [allow?: "yes"])
{:error,
[
%Zoi.Error{
code: :invalid_type,
issue: {"invalid type: expected boolean", [type: :boolean]},
message: "invalid type: expected boolean",
path: [:allow?]
}
]}And many more possibilities, including nested schemas, custom validations and data transformations. Check the official docs for more details.
Types
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))
endThis 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))
endWhich will generate:
@type t :: %{
required(:name) => binary(),
optional(:age) => integer(),
required(:email) => binary()
}Errors
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{
code: :invalid_type,
issue: {"invalid type: expected string", [type: :string]},
message: "invalid type: expected string",
path: [:name]
},
%Zoi.Error{
code: :invalid_type,
issue: {"invalid type: expected integer", [type: :integer]},
message: "invalid type: expected integer",
path: [:age]
}
]}You can view the error in a map format using the Zoi.treefy_errors/1 function:
iex> schema = Zoi.object(%{name: Zoi.string(), age: Zoi.integer()})
iex> {:error, errors} = Zoi.parse(schema, %{name: 123, age: "thirty"})
iex> Zoi.treefy_errors(errors)
%{
name: ["invalid type: expected string"],
age: ["invalid type: expected integer"]
}You can also customize error messages:
iex> schema = Zoi.string(error: "not a string")
iex> Zoi.parse(schema, :hi)
{:error,
[
%Zoi.Error{
code: :custom,
issue: {"not a string", [type: :string]},
message: "not a string",
path: []
}
]}Phoenix forms
Zoi works seamlessly with Phoenix forms through the Phoenix.HTML.FormData protocol:
# Define schema inline
@user_schema Zoi.object(%{
name: Zoi.string() |> Zoi.min(3),
email: Zoi.email()
}) |> Zoi.Form.prepare()
# Parse and render (just like changesets!)
ctx = Zoi.Form.parse(@user_schema, params)
form = to_form(ctx, as: :user)
socket |> assign(:form, form)
# Use in your forms
~H"""
<.form for={@form} phx-submit="save">
<.input field={@form[:name]} label="Name" />
<.input field={@form[:email]} label="Email" />
<div>
<.button>Save</.button>
</div>
</.form>
"""- See Rendering forms with Phoenix for a complete LiveView example.
- See Localizing errors with Gettext for translation support.
Metadata
Zoi supports 3 types of metadata:
description: Description of the schema.example: An example value that conforms to the schema.metadata: A keyword list of arbitrary metadata.
You can use in all types, for example:
iex> schema = Zoi.string(description: "Hello", example: "World!", metadata: [identifier: "string"])
iex> Zoi.description(schema)
"Hello"
iex> Zoi.example(schema)
"World!"
iex> Zoi.metadata(schema)
[identifier: "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(description: "The user first name") |> Zoi.min(2) |> Zoi.max(100),
age: Zoi.integer(description: "The user age") |> Zoi.optional()
},
description: "A user schema with name and optional age",
example: %{name: "Alice", age: 30},
metadata: [
moduledoc: "This module represents a schema of a user"
]
)
@moduledoc """
#{Zoi.metadata(@schema)[:moduledoc]}
"""
@doc """
#{Zoi.description(@schema)}
Options:
#{Zoi.describe(@schema)}
"""
def schema, do: @schema
end
defmodule MyApp.UserSchemaTest do
use ExUnit.Case
alias MyApp.UserSchema
test "example matches schema" do
example = Zoi.example(UserSchema.schema())
assert {:ok, example} == Zoi.parse(UserSchema.schema(), example)
end
enddescription, example are also used when generating OpenAPI specs. See the Using Zoi to generate OpenAPI specs guide for more details.
Guides
Check the official guides for more examples and use cases:
- Quickstart Guide
- Recipes
- Main API Reference
- Using Zoi to generate OpenAPI specs
- Validating controller parameters
- Converting Keys From Object
- Generating Schemas from JSON
Acknowledgements
Zoi is inspired by different schema validation libraries, including: