guides/03_structs_model_validators_computed_fields.md

# Structs, Model Validators, and Computed Fields

This guide explains Exdantic's full compile-time validation pipeline and how struct output, model-level logic, and derived fields interact.

## Pipeline Order

`Exdantic.StructValidator` executes the following steps in order:

1. Field validation
2. Model validators
3. Computed fields
4. Optional struct creation

Each stage can stop the pipeline with structured errors.

## Struct Output (`define_struct: true`)

Enable struct output at module declaration:

```elixir
defmodule UserSchema do
  use Exdantic, define_struct: true

  schema do
    field :name, :string
  end
end
```

`validate/1` now returns `%UserSchema{...}` on success.

Additional generated functions:

- `__struct_enabled__?/0`
- `__struct_fields__/0`
- `__regular_fields__/0`
- `__computed_field_names__/0`
- `dump/1` (struct/map to map)

Without `define_struct: true`, validation returns plain maps.

## Model Validators

Model validators run after all field-level validation passes.

### Named function

```elixir
model_validator :normalize_email

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

### Anonymous function

```elixir
model_validator fn input ->
  if input.start_date <= input.end_date do
    {:ok, input}
  else
    {:error, "start_date must be <= end_date"}
  end
end
```

### `do ... end` block style

```elixir
model_validator do
  if String.contains?(input.email, "@") do
    {:ok, input}
  else
    {:error, "invalid email"}
  end
end
```

### Return contract

Model validator functions must return one of:

- `{:ok, updated_data}`
- `{:error, "message"}`
- `{:error, %Exdantic.Error{}}`
- `{:error, [%Exdantic.Error{}, ...]}`

Validators run in declaration order.

## Computed Fields

Computed fields add derived values after model validators complete.

### Named function

```elixir
computed_field :full_name, :string, :compute_full_name

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

### Anonymous function

```elixir
computed_field :initials, :string, fn input ->
  {:ok, String.first(input.first_name) <> String.first(input.last_name)}
end
```

### Metadata options

Named form supports extra metadata:

```elixir
computed_field :email_domain, :string, :domain,
  description: "Domain extracted from email",
  example: "company.com"
```

### Type checking of computed outputs

Computed values are validated against the declared computed field type.

If a function returns a mismatched value, Exdantic emits computed-field type errors with field path and function context.

## Schema Introspection APIs

All compile-time schemas expose:

- `__schema__(:fields)`
- `__schema__(:computed_fields)`
- `__schema__(:model_validators)`
- `__schema__(:config)`
- `__schema_info__/0`

Enhanced metadata/reporting helpers:

- `__enhanced_schema_info__/0`
- `validate_enhanced/2`

`validate_enhanced/2` can include optional metrics and schema metadata.

## Error Behavior in Pipeline Stages

Field stage errors:

- Required fields missing
- Type mismatches
- Constraint failures

Model stage errors:

- Invalid validator return format
- Explicit validator errors
- Exceptions in validator function

Computed stage errors:

- Missing function
- Execution failure
- Invalid return shape
- Computed value fails declared type

Struct stage errors:

- Invalid data keys for struct creation
- Unexpected fields introduced by transformations

## Practical Pattern

A common production pattern:

1. Field constraints handle local validation
2. Model validators normalize and enforce cross-field invariants
3. Computed fields generate output-ready derived values
4. Struct output gives clear typed result shape for downstream code

## Next Guides

- `guides/04_runtime_schemas.md`
- `guides/05_type_adapter_wrapper_root_schema.md`