Structs, Model Validators, and Computed Fields

Copy Markdown View Source

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:

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

model_validator :normalize_email

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

Anonymous function

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

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

computed_field :full_name, :string, :compute_full_name

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

Anonymous function

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:

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