# `AshZoi`
[🔗](https://github.com/Munksgaard/ash_zoi/blob/main/lib/ash_zoi.ex#L1)

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()` or `Zoi.enum()` (with `one_of` constraint)
- `Ash.Type.Decimal` → `Zoi.decimal()`
- `AshMoney.Types.Money` → `Zoi.map(%{currency: Zoi.string(), amount: Zoi.decimal()})` (requires optional `ash_money` dependency)
- `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 optional `fields` constraint)
- `Ash.Type.Struct` → `Zoi.struct()` (with `instance_of` and `fields`)
- `Ash.Type.Module` → `Zoi.module()`
- `Ash.Type.Union` → `Zoi.discriminated_union()` (using `_union_type`/`_union_value` format)
- `Ash.Type.Enum` → `Zoi.enum()` (custom enum types defined with `use Ash.Type.Enum`)
- Ash Resources → `Zoi.map()` (introspected from resource attributes)
- `Ash.Type.NewType` → Recursively resolved to underlying subtype
- `Ash.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?` 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

# `to_schema`

```elixir
@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"]}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
