# `Exdantic.Schema`
[🔗](https://github.com/nshkrdotcom/exdantic/blob/v0.1.0/lib/exdantic/schema.ex#L1)

Schema DSL for defining data schemas with validation rules and metadata.

This module provides macros and functions for defining structured data schemas
with rich validation capabilities, type safety, and comprehensive error reporting.

## Phase 4 Enhancement: Anonymous Function Support

Added support for inline anonymous functions in model validators and computed fields:

    schema do
      field :password, :string
      field :password_confirmation, :string

      # Named function (existing)
      model_validator :validate_passwords_match

      # Anonymous function (new)
      model_validator fn input ->
        if input.password == input.password_confirmation do
          {:ok, input}
        else
          {:error, "passwords do not match"}
        end
      end

      # Anonymous function with do-end block (new)
      model_validator do
        if input.password == input.password_confirmation do
          {:ok, input}
        else
          {:error, "passwords do not match"}
        end
      end

      computed_field :display_name, :string do
        String.upcase(input.name)
      end
    end

# `macro_ast`

```elixir
@type macro_ast() :: term()
```

# `model_validator_ast`

```elixir
@type model_validator_ast() ::
  {:@, [context: Exdantic.Schema, imports: [...]],
   [{:model_validators, [...], [...]}]}
  | {:__block__, [], [{:def, [...], [...]} | {:@, [...], [...]}]}
```

# `schema_config`

```elixir
@type schema_config() :: %{
  optional(:title) =&gt; String.t(),
  optional(:description) =&gt; String.t(),
  optional(:strict) =&gt; boolean()
}
```

# `choices`
*macro* 

```elixir
@spec choices([term()]) :: Macro.t()
```

Adds an enumeration constraint, limiting values to a predefined set.

## Parameters
  * `values` - List of allowed values

## Examples

    field :status, :string do
      choices(["pending", "active", "completed"])
    end

    field :priority, :integer do
      choices([1, 2, 3])
    end

    field :size, :string do
      choices(["small", "medium", "large"])
    end

# `computed_field`
*macro* 

```elixir
@spec computed_field(atom(), term(), atom()) :: macro_ast()
@spec computed_field(atom(), term(), (map() -&gt;
                                  {:ok, term()}
                                  | {:error, String.t() | Exdantic.Error.t()})) ::
  macro_ast()
```

Defines a computed field that generates a value based on validated data.

Computed fields execute after field and model validation, generating additional
data that becomes part of the final validated result. They are particularly
useful for derived values, formatted representations, or aggregated data.

## Parameters
  * `name` - Field name (atom)
  * `type` - Field type specification (same as regular fields)
  * `function_name` - Name of the function to call for computation (atom) or anonymous function
  * `opts` - Optional keyword list with :description and :example (when using named function)

## Function Signature
The computation function must accept one parameter (the validated data) and return:
  * `{:ok, computed_value}` - computation succeeds
  * `{:error, message}` - computation fails with error message
  * `{:error, %Exdantic.Error{}}` - computation fails with detailed error

## Execution Order
Computed fields execute after:
1. Field validation
2. Model validation

This ensures computed fields have access to fully validated and transformed data.

## Examples

    # Using named function
    defmodule UserSchema do
      use Exdantic, define_struct: true

      schema do
        field :first_name, :string, required: true
        field :last_name, :string, required: true
        field :email, :string, required: true

        computed_field :full_name, :string, :generate_full_name
        computed_field :email_domain, :string, :extract_email_domain,
          description: "Domain part of the email address",
          example: "example.com"
      end

      def generate_full_name(input) do
        {:ok, "#{input.first_name} #{input.last_name}"}
      end

      def extract_email_domain(input) do
        domain = input.email |> String.split("@") |> List.last()
        {:ok, domain}
      end
    end

    # Using anonymous function
    schema do
      field :first_name, :string
      field :last_name, :string

      computed_field :full_name, :string, fn input ->
        {:ok, "#{input.first_name} #{input.last_name}"}
      end

      computed_field :initials, :string, fn input ->
        first = String.first(input.first_name)
        last = String.first(input.last_name)
        {:ok, "#{first}#{last}"}
      end
    end

## Error Handling

Computed field functions can return errors that will be included in validation results:

    def risky_computation(data) do
      if valid_computation?(data) do
        {:ok, compute_value(data)}
      else
        {:error, "Computation failed due to invalid data"}
      end
    end

## Type Safety

Computed field return values are validated against their declared types:

    computed_field :score, :integer, :calculate_score

    def calculate_score(data) do
      # This will fail validation if score is not an integer
      {:ok, "not an integer"}
    end

## JSON Schema Integration

Computed fields are automatically included in generated JSON schemas and marked as `readOnly`:

    %{
      "type" => "object",
      "properties" => %{
        "first_name" => %{"type" => "string"},
        "full_name" => %{"type" => "string", "readOnly" => true}
      }
    }

## With Struct Definition

When using `define_struct: true`, computed fields are included in the struct definition:

    defstruct [:first_name, :last_name, :email, :full_name, :email_domain]

# `computed_field`
*macro* 

```elixir
@spec computed_field(atom(), term(), atom(), description: String.t(), example: term()) ::
  macro_ast()
```

# `config`
*macro* 

```elixir
@spec config(keyword()) :: Macro.t()
```

Defines configuration settings for the schema.

Configuration options can include:
  * title - Schema title
  * description - Schema description
  * strict - Whether to enforce strict validation

## Examples

    config do
      title("User Schema")
      config_description("Validates user registration data")
      strict(true)
    end

    config do
      strict(false)
    end

# `config_description`
*macro* 

```elixir
@spec config_description(String.t()) :: Macro.t()
```

Sets the description for the schema configuration.

## Parameters
  * `text` - String description of the schema

## Examples

    config do
      config_description("Validates user data for registration")
    end

    config do
      title("User Schema")
      config_description("Comprehensive user validation with email format checking")
    end

# `default`
*macro* 

```elixir
@spec default(term()) :: Macro.t()
```

Sets a default value for the field and marks it as optional.
The default value will be used if the field is omitted from input data.

## Parameters
  * `value` - The default value to use when the field is not provided

## Examples

    field :status, :string do
      default("pending")
    end

    field :active, :boolean do
      default(true)
    end

    field :retry_count, :integer do
      default(0)
      gteq(0)
    end

# `description`
*macro* 

```elixir
@spec description(String.t()) :: Macro.t()
```

Sets a description for the field.

## Parameters
  * `text` - String description of the field's purpose or usage

## Examples

    field :age, :integer do
      description("User's age in years")
    end

    field :email, :string do
      description("Primary contact email address")
      format(~r/@/)
    end

# `example`
*macro* 

```elixir
@spec example(term()) :: Macro.t()
```

Sets a single example value for the field.

## Parameters
  * `value` - An example value that would be valid for this field

## Examples

    field :age, :integer do
      example(25)
    end

    field :name, :string do
      example("John Doe")
    end

# `examples`
*macro* 

```elixir
@spec examples([term()]) :: Macro.t()
```

Sets multiple example values for the field.

## Parameters
  * `values` - List of example values that would be valid for this field

## Examples

    field :status, :string do
      examples(["pending", "active", "completed"])
    end

    field :score, :integer do
      examples([85, 92, 78])
    end

# `extra`
*macro* 

```elixir
@spec extra(String.t(), term()) :: Macro.t()
```

Sets arbitrary extra metadata for the field.

This allows storing custom key-value pairs in the field metadata,
which is particularly useful for DSPy-style field type annotations
and other framework-specific metadata.

## Parameters
  * `key` - String key for the metadata
  * `value` - The metadata value

## Examples

    field :answer, :string do
      extra("__dspy_field_type", "output")
      extra("prefix", "Answer:")
    end

    field :question, :string do
      extra("__dspy_field_type", "input")
    end

    # Can also be used with map
    field :data, :string, extra: %{"custom_key" => "custom_value"}

# `field`
*macro* 

```elixir
@spec field(atom(), term(), keyword()) :: Macro.t()
@spec field(atom(), term(), keyword()) :: Macro.t()
```

Defines a field in the schema with a name, type, and optional constraints.

## Parameters
  * `name` - Atom representing the field name
  * `type` - The field's type, which can be:
    * A built-in type (`:string`, `:integer`, `:float`, `:boolean`, `:any`)
    * An array type (`{:array, type}`)
    * A map type (`{:map, {key_type, value_type}}`)
    * A union type (`{:union, [type1, type2, ...]}`)
    * A reference to another schema (atom)
  * `opts` - Optional block containing field constraints and metadata

## Examples

    # Simple field
    field :name, :string

    # Field with constraints
    field :age, :integer do
      description("User's age in years")
      gt(0)
      lt(150)
    end

    # Array field
    field :tags, {:array, :string} do
      min_items(1)
      max_items(10)
    end

    # Map field
    field :metadata, {:map, {:string, :any}}

    # Reference to another schema
    field :address, Address

    # Optional field with default
    field :active, :boolean do
      default(true)
    end

# `format`
*macro* 

```elixir
@spec format(Regex.t()) :: Macro.t()
```

Adds a format constraint to a string field.

## Parameters
  * `value` - The format pattern (regular expression)

## Examples

    field :email, :string do
      format(~r/^[^ @]+@[^ @]+.[^ @]+$/)
    end

    field :phone, :string do
      format(~r/^+?[1-9]{1,14}$/)
    end

# `gt`
*macro* 

```elixir
@spec gt(number()) :: Macro.t()
```

Adds a greater than constraint to a numeric field.

## Parameters
  * `value` - The minimum value (exclusive)

## Examples

    field :age, :integer do
      gt(0)
    end

    field :score, :float do
      gt(0.0)
      lt(100.0)
    end

# `gteq`
*macro* 

```elixir
@spec gteq(number()) :: Macro.t()
```

Adds a greater than or equal to constraint to a numeric field.

## Parameters
  * `value` - The minimum value (inclusive)

## Examples

    field :age, :integer do
      gteq(18)
    end

    field :rating, :float do
      gteq(0.0)
      lteq(5.0)
    end

# `lt`
*macro* 

```elixir
@spec lt(number()) :: Macro.t()
```

Adds a less than constraint to a numeric field.

## Parameters
  * `value` - The maximum value (exclusive)

## Examples

    field :age, :integer do
      lt(100)
    end

    field :temperature, :float do
      gt(-50.0)
      lt(100.0)
    end

# `lteq`
*macro* 

```elixir
@spec lteq(number()) :: Macro.t()
```

Adds a less than or equal to constraint to a numeric field.

## Parameters
  * `value` - The maximum value (inclusive)

## Examples

    field :rating, :float do
      lteq(5.0)
    end

    field :percentage, :integer do
      gteq(0)
      lteq(100)
    end

# `max_items`
*macro* 

```elixir
@spec max_items(non_neg_integer()) :: Macro.t()
```

Adds a maximum items constraint to an array field.

## Parameters
  * `value` - The maximum number of items allowed (must be a non-negative integer)

## Examples

    field :tags, {:array, :string} do
      max_items(10)
    end

    field :favorites, {:array, :integer} do
      min_items(1)
      max_items(3)
    end

# `max_length`
*macro* 

```elixir
@spec max_length(non_neg_integer()) :: Macro.t()
```

Adds a maximum length constraint to a string field.

## Parameters
  * `value` - The maximum length allowed (must be a non-negative integer)

## Examples

    field :username, :string do
      max_length(20)
    end

    field :description, :string do
      max_length(500)
    end

# `min_items`
*macro* 

```elixir
@spec min_items(non_neg_integer()) :: Macro.t()
```

Adds a minimum items constraint to an array field.

## Parameters
  * `value` - The minimum number of items required (must be a non-negative integer)

## Examples

    field :tags, {:array, :string} do
      min_items(1)
    end

    field :categories, {:array, :string} do
      min_items(2)
      max_items(5)
    end

# `min_length`
*macro* 

```elixir
@spec min_length(non_neg_integer()) :: Macro.t()
```

Adds a minimum length constraint to a string field.

## Parameters
  * `value` - The minimum length required (must be a non-negative integer)

## Examples

    field :username, :string do
      min_length(3)
    end

    field :password, :string do
      min_length(8)
      max_length(100)
    end

# `model_validator`
*macro* 

```elixir
@spec model_validator((map() -&gt;
                   {:ok, map()} | {:error, String.t() | Exdantic.Error.t()})) ::
  macro_ast()
@spec model_validator(atom()) :: macro_ast()
@spec model_validator(keyword()) :: macro_ast()
```

Defines a model-level validator that runs after field validation.

Model validators receive the validated data (as a map or struct) and can perform
cross-field validation, data transformation, or complex business logic validation.

## Parameters
  * `function_name` - Name of the function to call for model validation (when using named function)
  * `validator_fn` - Anonymous function that accepts validated data and returns result (when using anonymous function)
  * `do` block - Block of code with implicit `input` variable (when using do-end block)

## Function Signature
The validator must accept one parameter (the validated data) and return:
  * `{:ok, data}` - validation succeeds, optionally with transformed data
  * `{:error, message}` - validation fails with error message
  * `{:error, %Exdantic.Error{}}` - validation fails with detailed error

## Examples

    defmodule UserSchema do
      use Exdantic, define_struct: true

      schema do
        field :password, :string, required: true
        field :password_confirmation, :string, required: true

        # Using named function
        model_validator :validate_passwords_match

        # Using anonymous function
        model_validator fn input ->
          if input.password == input.password_confirmation do
            {:ok, input}
          else
            {:error, "passwords do not match"}
          end
        end

        # Using do-end block with implicit input
        model_validator do
          if input.password == input.password_confirmation do
            {:ok, input}
          else
            {:error, "passwords do not match"}
          end
        end
      end

      def validate_passwords_match(input) do
        if input.password == input.password_confirmation do
          {:ok, input}
        else
          {:error, "passwords do not match"}
        end
      end
    end

## Multiple Validators
Multiple model validators can be defined and will execute in the order they are declared:

    schema do
      field :username, :string, required: true
      field :email, :string, required: true

      model_validator :validate_username_unique
      model_validator :validate_email_format
      model_validator :send_welcome_email
    end

## Data Transformation
Model validators can transform the data by returning modified data:

    def normalize_email(input) do
      normalized = %{input | email: String.downcase(input.email)}
      {:ok, normalized}
    end

# `optional`
*macro* 

```elixir
@spec optional() :: Macro.t()
```

Marks the field as optional.
An optional field may be omitted from the input data during validation.

## Examples

    field :middle_name, :string do
      optional()
    end

    field :bio, :string do
      optional()
      max_length(500)
    end

# `required`
*macro* 

```elixir
@spec required() :: Macro.t()
```

Marks the field as required (this is the default behavior).
A required field must be present in the input data during validation.

## Examples

    field :email, :string do
      required()
      format(~r/@/)
    end

    field :name, :string do
      required()
      min_length(1)
    end

# `schema`
*macro* 

```elixir
@spec schema(
  String.t() | nil,
  keyword()
) :: Macro.t()
```

Defines a new schema with optional description.

## Parameters
  * `description` - Optional string describing the schema's purpose
  * `do` - Block containing field definitions and configuration

## Examples

    schema "User registration data" do
      field :name, :string do
        required()
        min_length(2)
      end

      field :age, :integer do
        optional()
        gt(0)
      end
    end

    schema do
      field :email, :string
      field :active, :boolean, default: true
    end

# `strict`
*macro* 

```elixir
@spec strict(boolean()) :: Macro.t()
```

Sets whether the schema should enforce strict validation.
When strict is true, unknown fields will cause validation to fail.

## Parameters
  * `bool` - Boolean indicating if strict validation should be enabled

## Examples

    config do
      strict(true)
    end

    config do
      title("Flexible Schema")
      strict(false)
    end

# `title`
*macro* 

```elixir
@spec title(String.t()) :: Macro.t()
```

Sets the title for the schema configuration.

## Parameters
  * `text` - String title for the schema

## Examples

    config do
      title("User Schema")
    end

    config do
      title("Product Validation Schema")
      strict(true)
    end

---

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