Zoi (Zoi v0.2.2)

View Source

Zoi is a schema validation library for Elixir, designed to provide a simple and flexible way to define and validate data.

It allows you to create schemas for various data types, including strings, integers, booleans, and complex objects, with built-in support for validations like minimum and maximum values, regex patterns, and email formats.

user = Zoi.object(%{
  name: Zoi.string() |> Zoi.min(2) |> Zoi.max(100),
  age: Zoi.integer() |> Zoi.min(18) |> Zoi.max(120),
  email: Zoi.string() |> Zoi.email()
})

Zoi.parse(user, %{
  name: "Alice",
  age: 30,
  email: "alice@email.com"
})
# {:ok, %{name: "Alice", age: 30, email: "alice@email.com"}}

Schemas

Zoi schemas are defined using a set of functions that create types and validations.

Primitive types:

Zoi.string()
Zoi.integer()
Zoi.float()
Zoi.number()
Zoi.boolean()

Encapsulated types:

Zoi.optional(inner_type)
Zoi.default(inner_type, default_value)
Zoi.union(fields)

Complex types:

Zoi.object(fields)
Zoi.enum(values)
Zoi.array(inner_type)
Zoi.tuple(inner_types)

Coercion

By default, Zoi will not attempt to infer input data to match the expected type. For example, if you define a schema that expects a string, passing an integer will result in an error.

iex> Zoi.string() |> Zoi.parse(123)
{:error, [%Zoi.Error{message: "invalid type: must be a string"}]}

If you need coercion, you can enable it by passing the :coerce option:

iex> Zoi.string(coerce: true) |> Zoi.parse(123)
{:ok, "123"}

Custom errors

You can customize parsing error messages the primitive types by passing the error option:

iex> schema = Zoi.integer(error: "must be a number")
iex> Zoi.parse(schema, "a")
{:error, [%Zoi.Error{message: "must be a number"}]}

Summary

Functions

Converts a list of errors into a tree structure, where each error is placed at its corresponding path.

Basic Types

Defines a schema that accepts any type of input.

Defines a boolean type schema.

Defines a float type schema.

Defines a number type schema.

Defines the numeric type schema.

Defines a string type schema.

Complex Types

Defines a array type schema.

Defines an enum type schema. Use Zoi.enum(values) to define a schema that accepts only specific values

Defines a object type schema.

Defines a tuple type schema.

Encapsulated Types

Creates a default value for the schema.

Defines a schema that allows nil values.

Makes the schema optional for the Zoi.object/2 type.

Defines a union type schema.

Extensions

Adds a custom validation function to the schema. This function will be called with the input data and options, and should return :ok for valid data or {:error, reason} for invalid data.

Adds a transformation function to the schema.

Parsing

Parse input data against a schema. Accepts optional coerce: true option to enable coercion.

Refinements

Validates that the string is a valid email format.

Validates that a string ends with a specific suffix.

Validates that the string has a specific length.

Validates that the input is less than or equal to a maximum value. This can be used for strings, integers, floats and numbers.

Validates that the input is greater than or equal to a minimum value. This can be used for strings, integers, floats and numbers.

Validates that the input matches a given regex pattern.

Validates that a string starts with a specific prefix.

Transforms

Converts a string to lowercase.

Converts a string to uppercase.

Trims whitespace from the beginning and end of a string.

Types

input()

@type input() :: any()

options()

@type options() :: keyword()

result()

@type result() :: {:ok, any()} | {:error, [Zoi.Error.t() | binary()]}

Functions

treefy_errors(errors)

@spec treefy_errors([Zoi.Error.t()]) :: map()

Converts a list of errors into a tree structure, where each error is placed at its corresponding path.

This is useful for displaying validation errors in a structured way, such as in a form.

Example

iex> errors = [
...>   %Zoi.Error{path: ["name"], message: "is required"},
...>   %Zoi.Error{path: ["age"], message: "must be a number"},
...>   %Zoi.Error{path: ["address", "city"], message: "is required"}
...> ]
iex> Zoi.treefy_errors(errors)
%{
  "name" => [%Zoi.Error{message: "is required"}],
  "age" => [%Zoi.Error{message: "must be a number"}],
  "address" => %{
    "city" => [%Zoi.Error{message: "is required"}]
  }
}

Basic Types

any(opts \\ [])

Defines a schema that accepts any type of input.

This is useful when you want to allow any data type without validation.

Example

iex> schema = Zoi.any()
iex> Zoi.parse(schema, "hello")
{:ok, "hello"}
iex> Zoi.parse(schema, 42)
{:ok, 42}
iex> Zoi.parse(schema, %{key: "value"})
{:ok, %{key: "value"}}

boolean(opts \\ [])

Defines a boolean type schema.

Example

iex> schema = Zoi.boolean()
iex> Zoi.parse(schema, true)
{:ok, true}

For coercion, you can pass the :coerce option:

iex> Zoi.boolean(coerce: true) |> Zoi.parse("true")
{:ok, true}

float(opts \\ [])

Defines a float type schema.

Example

iex> schema = Zoi.float()
iex> Zoi.parse(schema, 3.14)
{:ok, 3.14}

Built-in validations for floats include:

Zoi.min(0.0)
Zoi.max(100.0)

For coercion, you can pass the :coerce option:

iex> Zoi.float(coerce: true) |> Zoi.parse("3.14")
{:ok, 3.14}

integer(opts \\ [])

Defines a number type schema.

Example

iex> shema = Zoi.integer()
iex> Zoi.parse(shema, 42)
{:ok, 42}

Built-in validations for integers include:

Zoi.min(0)
Zoi.max(100)

number(opts \\ [])

Defines the numeric type schema.

This type is a union of Zoi.integer() and Zoi.float(), allowing you to validate both integers and floats.

Example

iex> schema = Zoi.number()
iex> Zoi.parse(schema, 42)
{:ok, 42}
iex> Zoi.parse(schema, 3.14)
{:ok, 3.14}

string(opts \\ [])

Defines a string type schema.

Example

Zoi provides built-in validations for strings, such as:

Zoi.min(2)
Zoi.max(100)
Zoi.length(5)
Zoi.regex(~r/^[a-zA-Z]+$/)

Additionally it can perform data transformation:

Zoi.string()
|> Zoi.trim()
|> Zoi.downcase()
|> Zoi.uppercase()

Zoi also supports validating formats:

Zoi.email()
# pattern ~r/^(?!.)(?!.*..)([a-z0-9_'+-.]*)[a-z0-9_+-]@([a-z0-9][a-z0-9-]*.)+[a-z]{2,}$/i

Complex Types

array(elements, opts \\ [])

Defines a array type schema.

Use Zoi.array(elements) to define an array of a specific type:

iex> schema = Zoi.array(Zoi.string())
iex> Zoi.parse(schema, ["hello", "world"])
{:ok, ["hello", "world"]}
iex> Zoi.parse(schema, ["hello", 123])
{:error, [%Zoi.Error{message: "invalid string type", path: [1]}]}

Built-in validations for integers include:

Zoi.min(3)
Zoi.max(10)
Zoi.length(5)

enum(values, opts \\ [])

Defines an enum type schema. Use Zoi.enum(values) to define a schema that accepts only specific values:

iex> schema = Zoi.enum([:red, :green, :blue])
iex> Zoi.parse(schema, :red)
{:ok, :red}
iex> Zoi.parse(schema, :yellow)
{:error, [%Zoi.Error{message: "invalid value for enum"}]}

You can also specify enum as strings:

iex> schema = Zoi.enum(["red", "green", "blue"])
iex> Zoi.parse(schema, "red")
{:ok, "red"}
iex> Zoi.parse(schema, "yellow")
{:error, [%Zoi.Error{message: "invalid value for enum"}]}

or with key-value pairs:

iex> schema = Zoi.enum([red: "Red", green: "Green", blue: "Blue"])
iex> Zoi.parse(schema, "Red")
{:ok, :red}
iex> Zoi.parse(schema, "Yellow")
{:error, [%Zoi.Error{message: "invalid value for enum"}]}

Integer values can also be used:

iex> schema = Zoi.enum([1, 2, 3])
iex> Zoi.parse(schema, 1)
{:ok, 1}
iex> Zoi.parse(schema, 4)
{:error, [%Zoi.Error{message: "invalid value for enum"}]}

And Integers with key-value pairs also is allowed:

iex> schema = Zoi.enum([one: 1, two: 2, three: 3])
iex> Zoi.parse(schema, 1)
{:ok, :one}
iex> Zoi.parse(schema, 4)
{:error, [%Zoi.Error{message: "invalid value for enum"}]}

object(fields, opts \\ [])

Defines a object type schema.

Use Zoi.object(fields) to define complex objects with nested schemas:

user_schema = Zoi.object(%{
  name: Zoi.string() |> Zoi.min(2) |> Zoi.max(100),
  age: Zoi.integer() |> Zoi.min(18) |> Zoi.max(120),
  email: Zoi.string() |> Zoi.email()
})

iex> Zoi.parse(user_schema, %{name: "Alice", age: 30, email: "alice@email.com"})
{:ok, %{name: "Alice", age: 30, email: "alice@email.com"}}

By default all fields are required, but you can make them optional by using Zoi.optional/1:

user_schema = Zoi.object(%{
  name: Zoi.string() |> Zoi.optional(),
  age: Zoi.integer() |> Zoi.optional(),
  email: Zoi.string() |> Zoi.email() |> Zoi.optional()
})

iex> Zoi.parse(user_schema, %{name: "Alice"})
{:ok, %{name: "Alice"}}

By default, unrecognized keys will be removed from the parsed data. If you want to not allow unrecognized keys, use the :strict option:

iex> schema = Zoi.object(%{name: Zoi.string()}, strict: true)
iex> Zoi.parse(schema, %{name: "Alice", age: 30})
{:error, [%Zoi.Error{message: "unrecognized key: 'age'"}]}

tuple(fields, opts \\ [])

Defines a tuple type schema.

Use Zoi.tuple(fields) to define a tuple with specific types for each element:

iex> schema = Zoi.tuple({Zoi.string(), Zoi.integer()})
iex> Zoi.parse(schema, {"hello", 42})
{:ok, {"hello", 42}}
iex> Zoi.parse(schema, {"hello", "world"})
{:error, [%Zoi.Error{message: "invalid type: must be an integer", path: [1]}]}

Encapsulated Types

default(inner, value, opts \\ [])

Creates a default value for the schema.

This allows you to specify a default value that will be used if the input is nil or not provided.

Example

iex> schema = Zoi.string() |> Zoi.default("default value")
iex> Zoi.parse(schema, nil)
{:ok, "default value"}

nullable(opts \\ [])

Defines a schema that allows nil values.

Examples

iex> schema = Zoi.string() |> Zoi.nullable()
iex> Zoi.parse(schema, nil)
{:ok, nil}
iex> Zoi.parse(schema, "hello")
{:ok, "hello"}

optional(opts \\ [])

Makes the schema optional for the Zoi.object/2 type.

Example

iex> schema = Zoi.object(%{name: Zoi.string() |> Zoi.optional()})
iex> Zoi.parse(schema, %{})
{:ok, %{}}

union(fields, opts \\ [])

Defines a union type schema.

Example

iex> schema = Zoi.union([Zoi.string(), Zoi.integer()])
iex> Zoi.parse(schema, "hello")
{:ok, "hello"}
iex> Zoi.parse(schema, 42)
{:ok, 42}
iex> Zoi.parse(schema, true)
{:error, [%Zoi.Error{message: "invalid type for union"}]}

This type also allows to define validations for each type in the union:

iex> schema = Zoi.union([
...>   Zoi.string() |> Zoi.min(2),
...>   Zoi.integer() |> Zoi.min(0)
...> ])
iex> Zoi.parse(schema, "hi")
{:error, [%Zoi.Error{message: "minimum length is 2"}]}
iex> Zoi.parse(schema, -1)
{:error, [%Zoi.Error{message: "minimum value is 0"}]}

If you define the validation on the union itself, it will apply to all types in the union:

iex> schema = Zoi.union([
...>   Zoi.string(),
...>   Zoi.integer()
...> ]) |> Zoi.min(3)
iex> Zoi.parse(schema, "hello")
{:ok, "hello"}
iex> Zoi.parse(schema, 2)
{:error, [%Zoi.Error{message: "minimum value is 3"}]}

Extensions

refine(schema, fun)

@spec refine(schema :: Zoi.Type.t(), fun :: Zoi.Types.Meta.refinement()) ::
  Zoi.Type.t()

Adds a custom validation function to the schema. This function will be called with the input data and options, and should return :ok for valid data or {:error, reason} for invalid data.

Example

iex> schema = Zoi.string() |> Zoi.refine(fn input, _opts ->
...>   if String.length(input) > 5 do
...>     :ok
...>   else
...>     {:error, "must be longer than 5 characters"}
...>   end
...> end)
iex> Zoi.parse(schema, "hello world")
{:ok, "hello world"}
iex> Zoi.parse(schema, "hi")
{:error, [%Zoi.Error{message: "must be longer than 5 characters"}]}

transform(schema, fun)

@spec transform(schema :: Zoi.Type.t(), fun :: Zoi.Types.Meta.transform()) ::
  Zoi.Type.t()
@spec transform(schema :: Zoi.Type.t(), fun :: function()) :: Zoi.Type.t()

Adds a transformation function to the schema.

This function will be applied to the input data after parsing but before validations.

Example

iex> schema = Zoi.string() |> Zoi.transform(&String.trim/1)
iex> Zoi.parse(schema, "  hello world  ")
{:ok, "hello world"}

Parsing

parse(schema, input, opts \\ [])

@spec parse(schema :: Zoi.Type.t(), input :: input(), opts :: options()) :: result()

Parse input data against a schema. Accepts optional coerce: true option to enable coercion.

Examples

iex> schema = Zoi.string() |> Zoi.min(2) |> Zoi.max(100)
iex> Zoi.parse(schema, "hello")
{:ok, "hello"}

iex> Zoi.parse(schema, "hi")
{:error, [%Zoi.Error{message: "minimum length is 2"}]}

iex> Zoi.parse(schema, 123, coerce: true)
{:ok, "123"}

Refinements

email(schema)

@spec email(schema :: Zoi.Type.t()) :: Zoi.Type.t()

Validates that the string is a valid email format.

Example

iex> schema = Zoi.string() |> Zoi.email()
iex> Zoi.parse(schema, "test@test.com")
{:ok, "test@test.com"}
iex> Zoi.parse(schema, "invalid-email")
{:error, [%Zoi.Error{message: "invalid email format"}]}

ends_with(schema, suffix)

@spec ends_with(schema :: Zoi.Type.t(), suffix :: binary()) :: Zoi.Type.t()

Validates that a string ends with a specific suffix.

Example

iex> schema = Zoi.string() |> Zoi.ends_with("world")
iex> Zoi.parse(schema, "hello world")
{:ok, "hello world"}
iex> Zoi.parse(schema, "hello")
{:error, [%Zoi.Error{message: "must end with 'world'"}]}

length(schema, length)

@spec length(schema :: Zoi.Type.t(), length :: non_neg_integer()) :: Zoi.Type.t()

Validates that the string has a specific length.

Example

iex> schema = Zoi.string() |> Zoi.length(5)
iex> Zoi.parse(schema, "hello")
{:ok, "hello"}
iex> Zoi.parse(schema, "hi")
{:error, [%Zoi.Error{message: "length must be 5"}]}

max(schema, max)

Validates that the input is less than or equal to a maximum value. This can be used for strings, integers, floats and numbers.

Example

iex> schema = Zoi.string() |> Zoi.max(5)
iex> Zoi.parse(schema, "hello")
{:ok, "hello"}
iex> Zoi.parse(schema, "hello world")
{:error, [%Zoi.Error{message: "maximum length is 5"}]}

min(schema, min)

@spec min(schema :: Zoi.Type.t(), min :: non_neg_integer()) :: Zoi.Type.t()

Validates that the input is greater than or equal to a minimum value. This can be used for strings, integers, floats and numbers.

Example

iex> schema = Zoi.string() |> Zoi.min(2)
iex> Zoi.parse(schema, "hello")
{:ok, "hello"}
iex> Zoi.parse(schema, "hi")
{:error, [%Zoi.Error{message: "minimum length is 2"}]}

regex(schema, regex, opts \\ [])

Validates that the input matches a given regex pattern.

Example

iex> schema = Zoi.string() |> Zoi.regex(~r/^+$/)
iex> Zoi.parse(schema, "12345")
{:ok, "12345"}

starts_with(schema, prefix)

@spec starts_with(schema :: Zoi.Type.t(), prefix :: binary()) :: Zoi.Type.t()

Validates that a string starts with a specific prefix.

Example

iex> schema = Zoi.string() |> Zoi.starts_with("hello")
iex> Zoi.parse(schema, "hello world")
{:ok, "hello world"}
iex> Zoi.parse(schema, "world hello")
{:error, [%Zoi.Error{message: "must start with 'hello'"}]}

Transforms

to_downcase(schema)

Converts a string to lowercase.

Example

iex> schema = Zoi.string() |> Zoi.to_downcase()
iex> Zoi.parse(schema, "Hello World")
{:ok, "hello world"}

to_upcase(schema)

@spec to_upcase(schema :: Zoi.Type.t()) :: Zoi.Type.t()

Converts a string to uppercase.

Example

iex> schema = Zoi.string() |> Zoi.to_upcase()
iex> Zoi.parse(schema, "Hello World")
{:ok, "HELLO WORLD"}

trim(schema)

@spec trim(schema :: Zoi.Type.t()) :: Zoi.Type.t()

Trims whitespace from the beginning and end of a string.

Example

iex> schema = Zoi.string() |> Zoi.trim()
iex> Zoi.parse(schema, "  hello world  ")
{:ok, "hello world"}