Changelog

View Source

All notable changes to this project will be documented in this file.

0.17.0 - 2026-01-30

Added

  • unrecognized_keys option for Zoi.map/2, Zoi.object/2, Zoi.keyword/2, and Zoi.struct/3 to control how unrecognized keys are handled:
    • :strip (default) - removes unrecognized keys from the output
    • :error - returns an error when unrecognized keys are present
    • :preserve - keeps unrecognized keys in the output without validation (not available for structs)
    • {:preserve, {key_schema, value_schema}} - preserves unrecognized keys and validates them against the given schemas (not available for structs)
  • deprecated option for all schema types to emit deprecation warnings during parsing. Zoi.describe/1 will also include the deprecation message in the generated documentation
  • Zoi.to_json_schema/1 now emits deprecated: true when a schema is marked as deprecated
  • Multi-line description support in Zoi.describe/1 with proper indentation

Changed

  • strict option is now deprecated in favor of unrecognized_keys. Use unrecognized_keys: :error instead of strict: true
  • Zoi.extend/3 now uses options from schema1 instead of merging options from both schemas
  • Improved Zoi.Describe.Encoder output format:
    • Enum now uses | separator instead of "one of"
    • Union now uses | separator instead of "or"
    • Tuple now uses {X, Y} format instead of "tuple of X, Y values"

0.16.1 - 2026-01-20

Added

  • Zoi.lazy/1 now supports MFA tuples {module, function, args} in addition to anonymous functions. This enables lazy schemas to be stored in module attributes and used at compile time:

    # MFA tuple, can be sued runtime and compiletime
    @schema Zoi.lazy({MyModule, :user_schema, []})
    
    # Anonymous function, can be used runtime but not compiletime (like module attributes)
    Zoi.lazy(fn -> user_schema() end)

0.16.0 - 2026-01-19

Added

Changed

  • Add sponsor section to documentation (#140)
  • Fix Elixir 1.20 deprecation warnings (#141)
  • Fix string keys parsing on maps when coercion is enabled (#139)

0.15.0 - 2026-01-05

Added

  • Zoi.pid/1 type for validating pid values
  • Zoi.module/1 type for validating module values
  • Zoi.reference/1 type for validating reference values
  • Zoi.port/1 type for validating port values
  • Zoi.macro/1 type for validating quoted expressions (Macro.t())
  • typespec option for all types to override generated typespec:
    Zoi.integer(gte: 0, typespec: quote(do: non_neg_integer()))
    Zoi.any(typespec: quote(do: pos_integer()))

0.14.1 - 2026-01-02

Changed

0.14.0 - 2025-12-22

Added

  • Zoi.map/2 now supports field-based mode with %{field: type} notation, following Elixir's type system where fields are required by default
  • Zoi.function/1 type for validating function values with optional arity constraint
  • Zoi.struct/1 now accepts just a module to validate struct type without field validation
  • Zoi.Describe.Encoder protocol for generating human-readable type descriptions
  • Zoi.json/1 type for validating any JSON-compatible value (string, number, boolean, null, array, or object with string keys)
  • Zoi.map/2 now accepts coerce: true option to convert structs to maps via Map.from_struct/1, enabling validation of database structs for API output

Changed

  • Zoi.object/2 is now an alias for field-based Zoi.map/2. Both functions work identically
  • Zoi.Types.Object has been consolidated into Zoi.Types.Map. The Zoi.object/2 API remains unchanged
  • Zoi.keyword/2 default behavior: defaults now apply correctly when parsing keyword list with missing keys

0.13.1 - 2025-12-19

Changed

  • Add references on documentation of 0.13 release

0.13.0 - 2025-12-19

Added

  • Zoi.codec/3 for bidirectional transformations between types. Codecs enable parsing from one type to another and encoding back:

    date_codec = Zoi.codec(
      Zoi.ISO.date(),
      Zoi.date(),
      decode: fn value -> Date.from_iso8601(value) end,
      encode: fn value -> Date.to_iso8601(value) end
    )
    
    {:ok, ~D[2025-01-15]} = Zoi.parse(date_codec, "2025-01-15")
    {:ok, "2025-01-15"} = Zoi.encode(date_codec, ~D[2025-01-15])
  • Zoi.encode/3 and Zoi.encode!/3 functions to encode values using a codec's encode function.

  • Zoi.multiple_of/3 refinement for validating that a number is a multiple of a given value. Works with integer/0, float/0, number/0, and decimal/0 types:

  • Zoi.TypeSpec protocol for opt-in Elixir type specification generation. Custom types can now implement this protocol separately from Zoi.Type:

    defimpl Zoi.TypeSpec do
      def spec(_schema, _opts) do
        quote(do: binary())
      end
    end

Changed

  • Zoi.JSONSchema.Encoder for Zoi.object/2 now respects the strict option. When strict: true, the generated JSON Schema will have additionalProperties: false. When strict: false (default), it will have additionalProperties: true.
  • type_spec/2 moved from Zoi.Type protocol to the new Zoi.TypeSpec protocol. This is not a breaking change as the public API (Zoi.type_spec/1) remains unchanged.

0.12.1 - 2025-12-09

Added

0.12.0 - 2025-12-05

Added

  • Zoi.lazy/1 type for deferring schema evaluation until parse time, enabling recursive and self-referencing schemas.

0.11.1 - 2025-12-04

Added

  • Improved the documentation for "Rendering forms with Phoenix" guide, adding example on how to handle errors with changesets or custom errors.

0.11.0 - 2025-11-24

Changed

  • Protocol-based validation architecture: All validations now use Zoi.Validations.* protocols instead of the centralized Zoi.Refinements module. This improves:
    • Introspection: Constraint values stored as struct fields (e.g., min_length: 5) instead of opaque MFAs
    • Ergonomics: Pass constraints directly in constructors: Zoi.string(min_length: 5, max_length: 100)
    • Integration: External libraries can easily inspect schema constraints for JSON Schema, OpenAPI, etc.
  • Validation protocols: Gte, Lte, Gt, Lt, Length, Url, Regex, StartsWith, EndsWith, OneOf
  • Each type implements relevant protocols (String implements all, Integer/Float/Number implement Gte/Lte/Gt/Lt, etc.)
  • Now all types opts params are validated at type creation time, using Zoi internals, raising errors if invalid options are provided.
  • Zoi.gt/2 and Zoi.lt/2 refinements will now work with Zoi.integer(), Zoi.float() and Zoi.number() only. Zoi.array/2 and Zoi.string/2 types should use Zoi.min/2 and Zoi.max/2 instead for length validations.

0.10.7 - 2025-11-16

Added

  • Recipes guide with common use cases and examples of Zoi usage.

0.10.6 - 2025-11-13

Added

  • Zoi.one_of/2 type to accept a value that matches exactly one of the provided literal values.

0.10.5 - 2025-11-13

Changed

  • Zoi.enum/2 typespec for binary now returns binary() instead of literals.

0.10.4 - 2025-11-10

Changed

0.10.3 - 2025-11-10

Added

  • wrap Zoi.Type.t() into Zoi.schema() type
  • Group guides on hexdocs

0.10.2 - 2025-11-10

Added

  • Zoi.Schema.traverse/2 for recursively walking and transforming schema structures. This function applies a transformation to all nested fields while leaving the root schema unchanged, making it easy to apply operations like coercion, nullish, or defaults across an entire schema tree.
  • Zoi.coerce/1 helper function to enable type coercion on schemas that support it.

Changed

  • Zoi.transform/2 and Zoi.refine/2 are now chained in the order they were added, allowing more flexible validation and transformation flows.

0.10.1 - 2025-11-09

Added

0.10.0 - 2025-11-09

Added

  • Zoi.Form module with prepare/1 and parse/2 functions for seamless Phoenix form integration.
  • Phoenix.HTML.FormData protocol implementation for Zoi.Context, enabling Phoenix form rendering without losing the original params.
  • Partial parsing data is now preserved inside %Zoi.Context{} (and surfaced through forms) even when validation fails, allowing Phoenix forms to keep previously valid entries.
  • Keyword schemas defined with another schema as the value now keep the successfully parsed entries even if a sibling entry fails validation.
  • Zoi.Form.prepare/1 now forces coercion on every nested field so Phoenix form strings are cast into their target types automatically.
  • Zoi.Form.parse/2 automatically normalizes LiveView's map-based array format (with numeric string keys) into regular lists in ctx.input, eliminating the need for manual conversion when manipulating array fields dynamically.
  • Architecture diagram in main module documentation (Zoi) showing the parsing flow and validation pipeline with Mermaid visualization.

Changed

  • Achieved 100% test coverage across the entire codebase (previously 99.8%).

0.9.1 - 2025-11-06

Added

0.9.0 - 2025-11-06

Added

Changed

  • Zoi.type_spec/1 for object with string keys now returns generic map() type spec due to how Elixir handles this type internally.

0.9.0-rc.1 - 2025-11-04

Added

  • Zoi.object/2 and Zoi.keyword/2 now accept :empty_values option to define which values are considered empty when parsing objects and keyword lists. By default, this option is set to [], meaning no values are considered empty. You can customize this option to include values like nil, empty strings (""), or any other value you want to treat as empty and it will return a :required error when those values are encountered for required fields.

0.9.0-rc.0 - 2025-11-04

Changed

  • All errors have been reworked to include more context on the error code and issue. Now errors will have the following structure (example):
%Zoi.Error{
  code: :invalid_type,
  issue: {"invalid type: expected string", [expected: :string]},
  message: "invalid type: expected string",
  path: [:user, :name]
}

And it's also possible to have errors with dynamic messages:

%Zoi.Error{
  code: :invalid_literal,
  message: "invalid literal: expected true",
  issue: {"invalid literal: expected %{expected}", [expected: true]},
  path: []
}

This will give more flexibility when handling errors programmatically, and better support with tools such as Gettext for localization.

0.8.4 - 2025-11-01

Changed

0.8.3 - 2025-10-31

Added

  • All types now implements the Inspect protocol. This should improve the ergonomics when working with Zoi types in IEx or when inspecting/debugging it's types.

0.8.2 - 2025-10-30

Added

Changed

  • Zoi.keyword/2 now can accept a schema in the first argument to validate the values of the keyword list
  • Zoi.keyword/2 type_spec now reflects correctly the keyword list definition

0.8.1 - 2025-10-27

Changed

  • Update readme with new metadata examples and reference to main api

0.8.0 - 2025-10-26

Added

  • Zoi.nullish/2 type to accept nil or a value of a specific type
  • @spec for all public functions
  • @typedoc for all public types
  • Zoi.description/1 option to add description metadata to types for documentation purposes
  • Zoi.example/1 option to add example metadata to types for documentation purposes

Changed

  • Zoi.to_json_schema/1 now reads description, example opts from types to include them in the generated JSON Schema

0.7.4 - 2025-10-25

Changed

  • Zoi.regex/3 fix regex compilation, now the regex.opts are properly handled

0.7.3 - 2025-10-20

Added

  • Zoi.email/1 now accepts pattern option to customize the email regex

Changed

  • Zoi.enum/2 now accepts coerce option to coerce values to the key or to the value

0.7.2 - 2025-10-13

Added

  • Fixed example in guides/using_zoi_to_generate_openapi_specs.md

0.7.1 - 2025-10-12

Added

  • Zoi.to_json_schema/1 support for metadata (e.g., example, description)
  • guides/quickstart_guide.md added to the documentation

0.7.0 - 2025-10-10

Added

Changed

  • Zoi.array/2 fixed path in errors when parsing arrays
  • Zoi.regex/2 fixed regex compile errors when used in module attributes

0.6.6 - 2025-10-08

Added

  • Zoi.metadata/1 - option to add metadata to types for documentation purposes

Changed

0.6.5 - 2025-10-07

Added

  • Zoi.example/1 option to add example values to types for documentation and testing purposes

0.6.4 - 2025-09-30

Added

  • Zoi.downcase/1 refinement to validate if a string is in lowercase
  • Zoi.upcase/1 refinement to validate if a string is in uppercase
  • Zoi.hex/1 refinement to validate if a string is a valid hexadecimal

0.6.3 - 2025-09-27

Added

  • keys in Zoi.object/2 data structure
  • Zoi.struct/2 type to parse structs and maps into structs
  • Zoi.Struct module with helper functions to work with structs. This module offers two main functions:
    • Zoi.Struct.enforce_keys/1: List of keys that must be present in the struct
    • Zoi.Struct.struct_keys/1: List of keys and their default values to be used with defstruct

0.6.2 - 2025-09-26

Added

Changed

  • Refactor all errors to be generated on type creation instead of parsing time

0.6.1 - 2025-09-08

Added

0.6.0 - 2025-09-07

Added

  • Zoi.required/2 type to enforce presence of a value in keyword and object types

Changed

  • Zoi.object/2 now uses mfa to call inner transform function
  • Zoi.keyword/2 have all fields set as optional by default, use Zoi.required/2 to enforce presence of a value

0.5.7 - 2025-09-06

Changed

0.5.6 - 2025-09-05

Added

  • Zoi.parse!/3 function that raises an error if parsing fails
  • Zoi.type_spec/2 function that returns the Elixir type spec for a given Zoi schema, implemented for all types

0.5.5 - 2025-09-03

Added

Changed

0.5.4 - 2025-08-29

Added

  • Guide for converting keys from maps
  • Guide for generating schema from JSON structure

0.5.3 - 2025-08-29

Changed

  • Fix transform and refinement types

0.5.2 - 2025-08-28

Added

Changed

  • Zoi.map/3 now parses key and value types correctly
  • Fix encapsulated types ignoring refinements and transforms when parsing

0.5.1 - 2025-08-17

Changed

0.5.0 - 2025-08-17

Added

Changed

0.4.0 - 2025-08-14

Added

  • Zoi.Context module to provide context when parsing data

Changed

  • Zoi.object/2 will not automatically parse objects with inputs that differ from the string/atom keys map format. For example:
schema = Zoi.object(%{
  name: Zoi.string(),
  age: Zoi.integer()
})
Zoi.object(schema, %{"name" => "John", "age" => 30})
{:error, _errors}

To make this API work, you can pass coerce: true option to Zoi.object/2. This will make the object parser to check from the Map input if the keys are strings or atoms and fetch it's values automatically.

schema = Zoi.object(%{
  name: Zoi.string(),
  age: Zoi.integer()
})
Zoi.object(schema, %{"name" => "John", "age" => 30}, coerce: true)
{:ok, %{name: "John", age: 30}}

0.3.4 - 2025-08-09

Added

0.3.3 - 2025-08-09

Added

0.3.2 - 2025-08-09

Added

0.3.1 - 2025-08-08

Added

0.3.0 - 2025-08-07

Added

Changed

0.2.3 - 2025-08-06

Added

0.2.2 - 2025-08-06

Added

Changed

  • Improved error messages for all validations and types
  • Zoi.treefy_errors/1 now returns a more human-readable structure
  • Zoi.optional/2 cannot accept nil as a value anymore. Use Zoi.nullable/2 instead.
  • Zoi.optional/2 inside Zoi.object/2 now handles optional fields correctly

0.2.1 - 2025-08-06

Added

  • Custom error messages for primitive types

Changed

  • Zoi.number/2 now returns proper error message

0.2.0 - 2025-08-05

Added

Changed

  • errors are now returned as a list of %Zoi.Error{} structs