phcurado

phcurado

Zoi is a new schema validation library for Elixir.

It’s inspired by Zod from the JavaScript ecosystem, bringing a similar functional API for defining, validating, transforming and coercing data, using elixir pipes for the schema definition.

Basic Example

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

Transformations

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

Coercion

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

Complex schemas

iex> user_schema =
...>   Zoi.object(%{
...>     name: Zoi.string() |> Zoi.min(2) |> Zoi.max(100),
...>     age: Zoi.integer() |> Zoi.min(18) |> Zoi.max(120),
...>     email: Zoi.email()
...>   })
iex> Zoi.parse(user_schema, %{name: "Alice", age: 30, email: "alice@email.com"})
{:ok, %{name: "Alice", age: 30, email: "alice@email.com"}}

There are many built in types, validations and transformations. Check out the documentation for all the possibilities.


:books: Docs: Zoi — Zoi v0.18.4
:package: Hex: zoi | Hex
:laptop: Repo:

https://github.com/phcurado/zoi

I would love to hear your feedback, thoughts and any additional features you’d like to see

Showing Posts 1 to 10

th3mus1cman

th3mus1cman

Will have to check this out. I really like Zod so this looks pretty nice. Thanks!!

phcurado

phcurado OP

New 0.5 version released :rocket:

Added

atom type schema

Useful for verifying atom types:

schema = Zoi.atom()
Zoi.parse(schema, :hello)
#=> {:ok, :hello}

Zoi.parse(schema, "world")
#=> {:error, [%Zoi.Error{message: "invalid type: must be an atom", path: []}]}

which can be also useful for validating maps with atom keys:

schema = Zoi.map(Zoi.atom(), Zoi.string())
Zoi.parse(schema, %{name: "John"})
#=> {:ok, %{name: "John"}}

Zoi.parse(schema, %{"name" => "John"})
#=> {:error, [%Zoi.Error{message: "invalid type: must be an atom", path: ["name"]}]}

Union and intersection custom errors

Now we can give custom errors to unions (logical OR) and intersections (logical AND)

schema = Zoi.union([Zoi.float(), Zoi.integer()], error: "something went wrong")
Zoi.parse(schema, "not a number")
#=> {:error, [%Zoi.Error{message: "something went wrong", path: []}]}

string boolean type schema

useful when parsing boolean from different sources, good for environment variables or integration with external APIs:

schema = Zoi.string_boolean()
 Zoi.parse(schema, "true")
#=> {:ok, true}

Zoi.parse(schema, "1")
#=> {:ok, true}

Zoi.parse(schema, "off")
#=> {:ok, false}

Now Zoi have all types Zod have, with some additions to fit in the elixir ecosystem :rocket:

phcurado

phcurado OP

The recent release is focused on bug fixes, a new Zoi.extend/2 type and some guides.

Zoi.extend/2 type:

Extends two object type schemas into one.

user = Zoi.object(%{name: Zoi.string()})
role = Zoi.object(%{role: Zoi.enum([:admin,:user])})
user_with_role = Zoi.extend(user, role)
Zoi.parse(user_with_role, %{name: "Alice", role: :admin})
#=> {:ok, %{name: "Alice", role: :admin}}

This can help you when creating composable data structures.

:books: Guides

  • https://hexdocs.pm/zoi/converting-keys-from-object.html - This can be a very useful guide when you need to parse external request/responses by changing their format. For example converting camelCase to snakeCase or completely change how the schema needs to be presented in your application.
  • https://hexdocs.pm/zoi/validating-controller-parameters.html - A simple way to show how you can validate controller parameters using Zoi. Many applications will be fine by just using changesets directly, this is an alternative using Zoi and Changesets.
  • https://hexdocs.pm/zoi/generating-schemas-from-json-example.html - This can be an interesting article for people who work integrating with many external APIs. The common approach is to parse their external data structure to an internal data structure, usually it’s done using Changesets or plain Structs and Maps. It can be a tedius job to create the data structure and parsing logic, this guide focus on automatically generating a schema from a JSON response example.

Release v0.5.4: zoi | Hex

olivermt

olivermt

Would you be open to a parse!() PR?

Very convenient when you just want to blow up the world in runtime.exs on configuration errors.

phcurado

phcurado OP

Yes that’s a great idea

dimitarvp

dimitarvp

And then it’s your turn to open-source such a config library. :face_in_clouds:

phcurado

phcurado OP

New release :rocket:

This release is focused on bringing the keyword type and typespec inference from schema.

Keyword type

Defines a keyword list type schema.

schema = Zoi.keyword(name: Zoi.string(), age: Zoi.integer())
Zoi.parse(schema, [name: "Alice", age: 30])
#=> [name: "Alice", age: 30]}
Zoi.parse(schema, [name: "Alice"])
#=> {:error, [%Zoi.Error{message: "is required", path: [:age]}]}

Typespec generation

Generates the Elixir type specification for a given schema.

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 spec:
@type t :: binary()

All types in Zoi have their own type specification, even more complex ones:

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()
   }

New parsing function

As per @olivermt recommendation, we now have the Zoi.parse!/3 function for anyone who wants to raise errors when parsing:

schema = Zoi.string() |> Zoi.min(2) |> Zoi.max(100)
Zoi.parse!(schema, "h")
# ** (Zoi.ParseError) Parsing error:
#
# too small: must have at least 2 characters

Next steps

I believe with the current API, we can also do type inference for the new elixir type system and extend some features on top of the current types.
For example, I was experimenting automatically generating structs with defaults and typespecs using Zoi API:

defmodule User do
  import Zoi.Struct, only: [structure: 1]

  structure(%{name: Zoi.string(), age: Zoi.optional(Zoi.integer()), address: Zoi.default(Zoi.string(), "Unknown")})
end

This would create the User struct as follows:

defmodule User do
  @enforce_keys [:name, :address]
  @type t :: %User{name: binary(), age: integer() | nil, address: binary()}
  defstruct name: nil, age: nil, address: "Unknown"
end

I’m not sure if this is a good feature to support but in any case I can add a guide on how to achieve this and/or add it as a feature to the library.

Release v0.5.7: zoi | Hex

olivermt

olivermt

What would be nice is to enforce either or type config

Essentially a zoi required of either one or another.

My specific usecase is required config for a library I am making that needs either an s3 config or a directory config.

phcurado

phcurado OP

could you elaborate what you mean? If you have an example because I didn’t quite understand the problem you have

phcurado

phcurado OP

New release :rocket:

Two new types added to Zoi: struct and literal types

Struct type

Now Zoi supports struct types, to validate struct and it’s fields:

defmodule MyApp.User do
  defstruct [:name, :age, :email]
end

schema = Zoi.struct(MyApp.User, %{
  name: Zoi.string() |> Zoi.min(2) |> Zoi.max(100),
  age: Zoi.integer() |> Zoi.min(18) |> Zoi.max(120),
  email: Zoi.email()
})
Zoi.parse(schema, %MyApp.User{name: "Alice", age: 30, email: "alice@email.com"})
#=> {:ok, %MyApp.User{name: "Alice", age: 30, email: "alice@email.com"}}
Zoi.parse(schema, %{})
#=> {:error, "invalid type: must be a struct"}

by default it will try to validate the struct and it’s contents but you can also coerce the input (if it’s a map for example) to be converted to the struct:

schema = Zoi.struct(MyApp.User, %{
  name: Zoi.string(),
  age: Zoi.integer(),
  email: Zoi.email()
}, coerce: true)
Zoi.parse(schema, %{name: "Alice", age: 30, email: "alice@email.com"})
#=> {:ok, %MyApp.User{name: "Alice", age: 30, email: "alice@email.com"}}
# Also with string keys
Zoi.parse(schema, %{"name" => "Alice", "age" => 30, "email" => "alice@email.com"})
#=> {:ok, %MyApp.User{name: "Alice", age: 30, email: "alice@email.com"}}

There are also some helpers for creating structs: struct fields, defaults and the @enforce_keys helper functions:

defmodule MyApp.User do
  @schema Zoi.struct(__MODULE__, %{
    name: Zoi.string()
    age: Zoi.integer() |> Zoi.default(0) |> Zoi.optional(),
    email: Zoi.string()
  })

  @enforce_keys Zoi.Struct.enforce_keys(schema) # [:name]
  defstruct Zoi.Struct.struct_fields(schema) # [:name, :email, {:age, 0}]
end

Literal type

As the name suggest, this type represents a literal value. The input value should always match the defined literal:

schema = Zoi.literal(true)
Zoi.parse(schema, true)
{:ok, true}
Zoi.parse(schema, :other_value)
{:error, [%Zoi.Error{message: "invalid type: does not match literal"}]}
schema = Zoi.literal(42)
Zoi.parse(schema, 42)
{:ok, 42}
Zoi.parse(schema, 43)
{:error, [%Zoi.Error{message: "invalid type: does not match literal"}]}

Release v0.6.3: zoi | Hex

Where Next? Top

Trending in Announcing Top

woylie
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries. offset-based pagination with...
New
MRdotB
I needed to reuse React components from my Chrome extension in my Phoenix/LiveView backend. I noticed that for Svelte/Vue, there are live...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
fuelen
Hi all! I want to present a small library which provides a mix task for generating an Entity-Relationship Diagram for Ecto schemas. You...
New
anuaralfetahe
Hello Published a new library - ProcessHub! ProcessHub is a library designed to manage process distribution within the Elixir cluster. ...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New
sergio
It’s not that it’s vocabulary is too advanced. It’s something worse. I get lost trying to follow even a paragraph written by Claude. It’...
New
AstonJ
This showed up on my feed.. anyone heard of it? Just hype? Ox Alpha is a reasoning model designed for coding, sustained ag...
New
sorenone
Today we’re releasing Oban for Python. Not an Oban client in Python. Not a pythonx wrapper embedded in Elixir. Nope, it’s a fully operati...
New
akoutmos
@hugobarauna, Dr. Dimitrios Koutmos (my brother) and I (Alex Koutmos) have been hard at work on writing a book on how you can use Elixir ...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews