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.Type.String→Zoi.string()Ash.Type.CiString→Zoi.string()(case-insensitive string, validated as string)Ash.Type.Integer→Zoi.integer()Ash.Type.Float→Zoi.float()Ash.Type.Boolean→Zoi.boolean()Ash.Type.Atom→Zoi.atom()orZoi.enum()(withone_ofconstraint)Ash.Type.Decimal→Zoi.decimal()Ash.Type.Date→Zoi.date()Ash.Type.Time→Zoi.time()Ash.Type.DateTime→Zoi.datetime()Ash.Type.NaiveDatetime→Zoi.naive_datetime()Ash.Type.UUID→Zoi.uuid()Ash.Type.Map→Zoi.map()(with optionalfieldsconstraint)Ash.Type.Struct→Zoi.struct()(withinstance_ofandfields)Ash.Type.Module→Zoi.module()Ash.Type.Union→Zoi.discriminated_union()(using_union_type/_union_valueformat)- Ash Resources →
Zoi.map()(introspected from resource attributes) Ash.Type.NewType→ Recursively resolved to underlying subtypeAsh.TypedStruct→Zoi.map()(introspected from typed struct fields)- Other types →
Zoi.any()
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,match→regex - Integer/Float:
min→gte,max→lte,greater_than→gt,less_than→lt - Atom:
one_of→Zoi.enum/1 - Array:
min_length,max_length,items(element constraints) - Struct:
instance_of(struct module),fields(typed fields)
Limitations
- Array constraints
nil_items?andremove_nil_items?are not supported - Decimal constraints
precisionandscaleare ignored - DateTime constraints
precision,cast_dates_as,timezoneare ignored - Time constraint
precisionis ignored
Behavior Notes
- Ash resource attributes have
allow_nil?: trueby default, making them nullable in the Zoi schema. Setallow_nil?: falseon your Ash attributes to make them required in the generated schema. - Map field definitions (
:maptype with:fieldsconstraint) defaultallow_nil?tofalse, matching Ash's map field defaults. - Constraints that don't apply to a type are silently ignored
- Map fields without a
:typedefault 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
@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:onlyand:exceptoptions to control which attributes are included in the schema.
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"]}