AshZoi (AshZoi v0.4.0)

Copy Markdown View Source

Bridges Ash types to Zoi validation schemas.

AshZoi provides a simple way to convert Ash type definitions (with constraints) into Zoi validation schemas that can be used for runtime validation.

Example

# Basic type conversion
AshZoi.to_schema(:string)
#=> Zoi.string()

# With constraints
AshZoi.to_schema(:string, min_length: 3, max_length: 100)
#=> Zoi.string(min_length: 3, max_length: 100)

# Array types
AshZoi.to_schema({:array, :integer}, min_length: 1, items: [min: 0, max: 100])
#=> Zoi.array(Zoi.integer(gte: 0, lte: 100), min_length: 1)

# Map types with fields
AshZoi.to_schema(:map, fields: [
  name: [type: :string, constraints: [min_length: 1]],
  age: [type: :integer]
])
#=> Zoi.map(%{name: Zoi.string(min_length: 1), age: Zoi.integer()})

# Ash resources
AshZoi.to_schema(MyApp.User)
#=> Zoi.map(%{name: ..., email: ..., age: ...})

# Ash TypedStructs
AshZoi.to_schema(MyProfile)
#=> Zoi.map(%{username: ..., age: ..., bio: ...})

Type Mapping

The following Ash types are mapped to their Zoi equivalents:

Ash Resource Support

When you pass an Ash resource module to to_schema/2, it will introspect the resource's public attributes and generate a Zoi map schema:

defmodule MyApp.User do
  use Ash.Resource

  attributes do
    attribute :name, :string, allow_nil?: false
    attribute :email, :string, allow_nil?: false
    attribute :age, :integer, constraints: [min: 0, max: 150]
  end
end

# All public attributes
AshZoi.to_schema(MyApp.User)

# Only specific attributes
AshZoi.to_schema(MyApp.User, only: [:name, :email])

# Exclude specific attributes
AshZoi.to_schema(MyApp.User, except: [:age])

TypedStruct Support

Ash TypedStructs are fully supported and automatically converted to map schemas with field validation:

defmodule MyProfile do
  use Ash.TypedStruct

  typed_struct do
    field :username, :string, allow_nil?: false
    field :age, :integer, constraints: [min: 0, max: 150]
    field :bio, :string
  end
end

# Converts to a map schema with field validation
AshZoi.to_schema(MyProfile)
#=> Zoi.map(%{username: Zoi.string(), age: Zoi.integer(gte: 0, lte: 150), bio: Zoi.nullable(Zoi.string())})

NewType Support

Custom Ash.Type.NewType types are supported and recursively resolved to their underlying subtypes with constraints merged:

defmodule SSN do
  use Ash.Type.NewType, subtype_of: :string, constraints: [match: ~r/^{3}-{2}-{4}$/]
end

AshZoi.to_schema(SSN)
#=> Zoi.regex(Zoi.string(), ~r/^{3}-{2}-{4}$/)

# User-provided constraints override NewType defaults
AshZoi.to_schema(SSN, max_length: 11)
#=> Zoi.regex(Zoi.string(max_length: 11), ~r/^{3}-{2}-{4}$/)

Constraint Mapping

Ash constraints are mapped to Zoi options:

  • String: min_length, max_length, matchregex
  • Integer/Float: mingte, maxlte, greater_thangt, less_thanlt
  • Atom: one_ofZoi.enum/1
  • Array: min_length, max_length, items (element constraints)
  • Struct: instance_of (struct module), fields (typed fields)

Limitations

  • Array constraints nil_items? and remove_nil_items? are not supported
  • Decimal constraints precision and scale are ignored
  • DateTime constraints precision, cast_dates_as, timezone are ignored
  • Time constraint precision is ignored

Behavior Notes

  • Ash resource attributes have allow_nil?: true by default, making them nullable in the Zoi schema. Set allow_nil?: false on your Ash attributes to make them required in the generated schema.
  • Map field definitions (:map type with :fields constraint) default allow_nil? to false, matching Ash's map field defaults.
  • Constraints that don't apply to a type are silently ignored
  • Map fields without a :type default to :any
  • Unknown/unsupported Ash types fall back to Zoi.any()
  • Only public resource attributes are included by default

Summary

Functions

Converts an Ash type (with optional constraints) into a Zoi validation schema.

Functions

to_schema(type, constraints \\ [])

@spec to_schema(
  type :: atom() | module() | {:array, any()},
  constraints :: keyword() | nil
) :: struct()

Converts an Ash type (with optional constraints) into a Zoi validation schema.

Parameters

  • type - An Ash type atom (:string, :integer, etc.), module (Ash.Type.String), or array tuple ({:array, inner_type}).
  • constraints - A keyword list of Ash constraints to apply. For Ash resources, you can also pass :only and :except options to control which attributes are included in the schema.

Options

  • :coerce - When true, enables Zoi.coerce/1 on all nodes in the generated schema. This allows Zoi.parse/2 to automatically coerce values to the expected types — for example, converting JSON floats to Decimal, strings to enum atoms, and string map keys to atom keys. Useful when parsing raw JSON (e.g. from LLM responses) that needs to be cast through Ash types.

Examples

iex> schema = AshZoi.to_schema(:string)
iex> is_struct(schema)
true

iex> schema = AshZoi.to_schema(:integer, min: 0, max: 100)
iex> Zoi.parse(schema, 50)
{:ok, 50}

iex> schema = AshZoi.to_schema({:array, :string}, min_length: 1)
iex> Zoi.parse(schema, ["hello"])
{:ok, ["hello"]}