Packages
zoi
0.2.2
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.1
0.16.0
0.15.0
0.14.1
0.14.0
0.13.1
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.7
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.1
0.9.0
0.9.0-rc.1
0.9.0-rc.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Zoi is a schema validation library for Elixir, designed to provide a simple and flexible way to define and validate data.
Current section
74 Versions
Jump to
Current section
74 Versions
Compare versions
17
files changed
+270
additions
-80
deletions
| @@ -2,6 +2,22 @@ | |
| 2 2 | |
| 3 3 | All notable changes to this project will be documented in this file. |
| 4 4 | |
| 5 | + ## 0.2.2 - 2025-08-06 |
| 6 | + |
| 7 | + ### Added |
| 8 | + |
| 9 | + - Guides for using `Zoi` in Phoenix controllers |
| 10 | + - New `Zoi.tuple/2` type |
| 11 | + - New `Zoi.any/1` type |
| 12 | + - New `Zoi.nullable/2` type |
| 13 | + |
| 14 | + ### Changed |
| 15 | + |
| 16 | + - Improved error messages for all validations and types |
| 17 | + - `Zoi.treefy_errors/1` now returns a more human-readable structure |
| 18 | + - `Zoi.optional/2` cannot accept `nil` as a value anymore. Use `Zoi.nullable/2` instead. |
| 19 | + - `Zoi.optional/2` inside `Zoi.object/2` now handles optional fields correctly |
| 20 | + |
| 5 21 | ## 0.2.1 - 2025-08-06 |
| 6 22 | |
| 7 23 | ### Added |
| @@ -2,7 +2,7 @@ | |
| 2 2 | [{<<"Changelog">>,<<"https://hexdocs.pm/zoi/changelog.html">>}, |
| 3 3 | {<<"GitHub">>,<<"https://github.com/phcurado/zoi">>}]}. |
| 4 4 | {<<"name">>,<<"zoi">>}. |
| 5 | - {<<"version">>,<<"0.2.1">>}. |
| 5 | + {<<"version">>,<<"0.2.2">>}. |
| 6 6 | {<<"description">>, |
| 7 7 | <<"Zoi is a schema validation library for Elixir, designed to provide a simple and flexible way to define and validate data.">>}. |
| 8 8 | {<<"elixir">>,<<"~> 1.14">>}. |
| @@ -13,12 +13,14 @@ | |
| 13 13 | <<"lib/zoi/error.ex">>,<<"lib/zoi/refinements.ex">>,<<"lib/zoi/type">>, |
| 14 14 | <<"lib/zoi/type/def.ex">>,<<"lib/zoi/types">>, |
| 15 15 | <<"lib/zoi/types/integer.ex">>,<<"lib/zoi/types/default.ex">>, |
| 16 | - <<"lib/zoi/types/number.ex">>,<<"lib/zoi/types/object.ex">>, |
| 17 | - <<"lib/zoi/types/optional.ex">>,<<"lib/zoi/types/float.ex">>, |
| 16 | + <<"lib/zoi/types/number.ex">>,<<"lib/zoi/types/nullable.ex">>, |
| 17 | + <<"lib/zoi/types/object.ex">>,<<"lib/zoi/types/optional.ex">>, |
| 18 | + <<"lib/zoi/types/tuple.ex">>,<<"lib/zoi/types/float.ex">>, |
| 18 19 | <<"lib/zoi/types/string.ex">>,<<"lib/zoi/types/boolean.ex">>, |
| 19 20 | <<"lib/zoi/types/enum.ex">>,<<"lib/zoi/types/array.ex">>, |
| 20 21 | <<"lib/zoi/types/union.ex">>,<<"lib/zoi/types/meta.ex">>, |
| 21 | - <<"lib/zoi/type.ex">>,<<"lib/zoi/errors.ex">>,<<"lib/zoi/transforms.ex">>, |
| 22 | - <<"mix.exs">>,<<"README.md">>,<<"CHANGELOG.md">>]}. |
| 22 | + <<"lib/zoi/types/any.ex">>,<<"lib/zoi/type.ex">>,<<"lib/zoi/errors.ex">>, |
| 23 | + <<"lib/zoi/transforms.ex">>,<<"mix.exs">>,<<"README.md">>, |
| 24 | + <<"CHANGELOG.md">>]}. |
| 23 25 | {<<"requirements">>,[]}. |
| 24 26 | {<<"build_tools">>,[<<"mix">>]}. |
| @@ -43,7 +43,7 @@ defmodule Zoi do | |
| 43 43 | ## Coercion |
| 44 44 | 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. |
| 45 45 | iex> Zoi.string() |> Zoi.parse(123) |
| 46 | - {:error, [%Zoi.Error{message: "invalid string type"}]} |
| 46 | + {:error, [%Zoi.Error{message: "invalid type: must be a string"}]} |
| 47 47 | |
| 48 48 | If you need coercion, you can enable it by passing the `:coerce` option: |
| 49 49 | |
| @@ -192,6 +192,24 @@ defmodule Zoi do | |
| 192 192 | @doc group: "Basic Types" |
| 193 193 | defdelegate boolean(opts \\ []), to: Zoi.Types.Boolean, as: :new |
| 194 194 | |
| 195 | + @doc """ |
| 196 | + Defines a schema that accepts any type of input. |
| 197 | + |
| 198 | + This is useful when you want to allow any data type without validation. |
| 199 | + |
| 200 | + ## Example |
| 201 | + |
| 202 | + iex> schema = Zoi.any() |
| 203 | + iex> Zoi.parse(schema, "hello") |
| 204 | + {:ok, "hello"} |
| 205 | + iex> Zoi.parse(schema, 42) |
| 206 | + {:ok, 42} |
| 207 | + iex> Zoi.parse(schema, %{key: "value"}) |
| 208 | + {:ok, %{key: "value"}} |
| 209 | + """ |
| 210 | + @doc group: "Basic Types" |
| 211 | + defdelegate any(opts \\ []), to: Zoi.Types.Any, as: :new |
| 212 | + |
| 195 213 | @doc """ |
| 196 214 | Makes the schema optional for the `Zoi.object/2` type. |
| 197 215 | |
| @@ -204,6 +222,19 @@ defmodule Zoi do | |
| 204 222 | @doc group: "Encapsulated Types" |
| 205 223 | defdelegate optional(opts \\ []), to: Zoi.Types.Optional, as: :new |
| 206 224 | |
| 225 | + @doc """ |
| 226 | + Defines a schema that allows `nil` values. |
| 227 | + |
| 228 | + ## Examples |
| 229 | + iex> schema = Zoi.string() |> Zoi.nullable() |
| 230 | + iex> Zoi.parse(schema, nil) |
| 231 | + {:ok, nil} |
| 232 | + iex> Zoi.parse(schema, "hello") |
| 233 | + {:ok, "hello"} |
| 234 | + """ |
| 235 | + @doc group: "Encapsulated Types" |
| 236 | + defdelegate nullable(opts \\ []), to: Zoi.Types.Nullable, as: :new |
| 237 | + |
| 207 238 | @doc """ |
| 208 239 | Creates a default value for the schema. |
| 209 240 | |
| @@ -291,6 +322,20 @@ defmodule Zoi do | |
| 291 322 | @doc group: "Complex Types" |
| 292 323 | defdelegate object(fields, opts \\ []), to: Zoi.Types.Object, as: :new |
| 293 324 | |
| 325 | + @doc """ |
| 326 | + Defines a tuple type schema. |
| 327 | + |
| 328 | + Use `Zoi.tuple(fields)` to define a tuple with specific types for each element: |
| 329 | + |
| 330 | + iex> schema = Zoi.tuple({Zoi.string(), Zoi.integer()}) |
| 331 | + iex> Zoi.parse(schema, {"hello", 42}) |
| 332 | + {:ok, {"hello", 42}} |
| 333 | + iex> Zoi.parse(schema, {"hello", "world"}) |
| 334 | + {:error, [%Zoi.Error{message: "invalid type: must be an integer", path: [1]}]} |
| 335 | + """ |
| 336 | + @doc group: "Complex Types" |
| 337 | + defdelegate tuple(fields, opts \\ []), to: Zoi.Types.Tuple, as: :new |
| 338 | + |
| 294 339 | @doc """ |
| 295 340 | Defines a array type schema. |
| 296 341 | |
| @@ -601,7 +646,7 @@ defmodule Zoi do | |
| 601 646 | @spec treefy_errors([Zoi.Error.t()]) :: map() |
| 602 647 | def treefy_errors(errors) when is_list(errors) do |
| 603 648 | Enum.reduce(errors, %{}, fn %Zoi.Error{path: path} = error, acc -> |
| 604 | - insert_error(acc, path, error) |
| 649 | + insert_error(acc, path, error.message) |
| 605 650 | end) |
| 606 651 | end |
| @@ -5,7 +5,7 @@ defmodule Zoi.Refinements do | |
| 5 5 | if String.length(input) >= min do |
| 6 6 | :ok |
| 7 7 | else |
| 8 | - {:error, "minimum length is #{min}"} |
| 8 | + {:error, "too small: must have at least #{min} characters"} |
| 9 9 | end |
| 10 10 | end |
| 11 11 | |
| @@ -13,7 +13,7 @@ defmodule Zoi.Refinements do | |
| 13 13 | if input >= min do |
| 14 14 | :ok |
| 15 15 | else |
| 16 | - {:error, "minimum value is #{min}"} |
| 16 | + {:error, "too small: must be at least #{min}"} |
| 17 17 | end |
| 18 18 | end |
| 19 19 | |
| @@ -21,7 +21,7 @@ defmodule Zoi.Refinements do | |
| 21 21 | if input >= min do |
| 22 22 | :ok |
| 23 23 | else |
| 24 | - {:error, "minimum value is #{min}"} |
| 24 | + {:error, "too small: must be at least #{min}"} |
| 25 25 | end |
| 26 26 | end |
| 27 27 | |
| @@ -29,7 +29,7 @@ defmodule Zoi.Refinements do | |
| 29 29 | if length(input) >= min do |
| 30 30 | :ok |
| 31 31 | else |
| 32 | - {:error, "minimum array length is #{min}"} |
| 32 | + {:error, "too small: must have at least #{min} items"} |
| 33 33 | end |
| 34 34 | end |
| 35 35 | |
| @@ -37,7 +37,7 @@ defmodule Zoi.Refinements do | |
| 37 37 | if String.length(input) <= max do |
| 38 38 | :ok |
| 39 39 | else |
| 40 | - {:error, "maximum length is #{max}"} |
| 40 | + {:error, "too big: must have at most #{max} characters"} |
| 41 41 | end |
| 42 42 | end |
| 43 43 | |
| @@ -45,7 +45,7 @@ defmodule Zoi.Refinements do | |
| 45 45 | if input <= max do |
| 46 46 | :ok |
| 47 47 | else |
| 48 | - {:error, "maximum value is #{max}"} |
| 48 | + {:error, "too big: must be at most #{max}"} |
| 49 49 | end |
| 50 50 | end |
| 51 51 | |
| @@ -53,7 +53,7 @@ defmodule Zoi.Refinements do | |
| 53 53 | if input <= max do |
| 54 54 | :ok |
| 55 55 | else |
| 56 | - {:error, "maximum value is #{max}"} |
| 56 | + {:error, "too big: must be at most #{max}"} |
| 57 57 | end |
| 58 58 | end |
| 59 59 | |
| @@ -61,7 +61,7 @@ defmodule Zoi.Refinements do | |
| 61 61 | if length(input) <= max do |
| 62 62 | :ok |
| 63 63 | else |
| 64 | - {:error, "maximum length is #{max}"} |
| 64 | + {:error, "too big: must have at most #{max} items"} |
| 65 65 | end |
| 66 66 | end |
| 67 67 | |
| @@ -69,7 +69,7 @@ defmodule Zoi.Refinements do | |
| 69 69 | if String.length(input) == length do |
| 70 70 | :ok |
| 71 71 | else |
| 72 | - {:error, "length must be #{length}"} |
| 72 | + {:error, "Invalid length: must have #{length} characters"} |
| 73 73 | end |
| 74 74 | end |
| 75 75 | |
| @@ -77,12 +77,13 @@ defmodule Zoi.Refinements do | |
| 77 77 | if length(input) == length do |
| 78 78 | :ok |
| 79 79 | else |
| 80 | - {:error, "length must be #{length}"} |
| 80 | + {:error, "Invalid length: must have #{length} items"} |
| 81 81 | end |
| 82 82 | end |
| 83 83 | |
| 84 84 | def refine(%Zoi.Types.String{}, input, [regex: regex], opts) do |
| 85 | - message = Keyword.get(opts, :message, "regex does not match") |
| 85 | + message = |
| 86 | + Keyword.get(opts, :message, "Invalid string: must match a patterh #{inspect(regex)}") |
| 86 87 | |
| 87 88 | if String.match?(input, regex) do |
| 88 89 | :ok |
| @@ -95,7 +96,7 @@ defmodule Zoi.Refinements do | |
| 95 96 | if String.starts_with?(input, prefix) do |
| 96 97 | :ok |
| 97 98 | else |
| 98 | - {:error, "must start with '#{prefix}'"} |
| 99 | + {:error, "Invalid string: must start with '#{prefix}'"} |
| 99 100 | end |
| 100 101 | end |
| 101 102 | |
| @@ -103,7 +104,7 @@ defmodule Zoi.Refinements do | |
| 103 104 | if String.ends_with?(input, suffix) do |
| 104 105 | :ok |
| 105 106 | else |
| 106 | - {:error, "must end with '#{suffix}'"} |
| 107 | + {:error, "Invalid string: must end with '#{suffix}'"} |
| 107 108 | end |
| 108 109 | end |
| @@ -0,0 +1,12 @@ | |
| 1 | + defmodule Zoi.Types.Any do |
| 2 | + @moduledoc false |
| 3 | + use Zoi.Type.Def |
| 4 | + |
| 5 | + def new(opts \\ []) do |
| 6 | + apply_type(opts) |
| 7 | + end |
| 8 | + |
| 9 | + defimpl Zoi.Type do |
| 10 | + def parse(%Zoi.Types.Any{}, value, _opts), do: {:ok, value} |
| 11 | + end |
| 12 | + end |
Loading more files…