Exdantic.Schema (exdantic v0.0.2)
View SourceSchema 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
Summary
Functions
Adds an enumeration constraint, limiting values to a predefined set.
Defines a computed field that generates a value based on validated data.
Defines configuration settings for the schema.
Sets the description for the schema configuration.
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.
Sets a description for the field.
Sets a single example value for the field.
Sets multiple example values for the field.
Sets arbitrary extra metadata for the field.
Defines a field in the schema with a name, type, and optional constraints.
Adds a format constraint to a string field.
Adds a greater than constraint to a numeric field.
Adds a greater than or equal to constraint to a numeric field.
Adds a less than constraint to a numeric field.
Adds a less than or equal to constraint to a numeric field.
Adds a maximum items constraint to an array field.
Adds a maximum length constraint to a string field.
Adds a minimum items constraint to an array field.
Adds a minimum length constraint to a string field.
Defines a model-level validator that runs after field validation.
Marks the field as optional. An optional field may be omitted from the input data during validation.
Marks the field as required (this is the default behavior). A required field must be present in the input data during validation.
Defines a new schema with optional description.
Sets whether the schema should enforce strict validation. When strict is true, unknown fields will cause validation to fail.
Sets the title for the schema configuration.
Types
@type macro_ast() :: term()
@type model_validator_ast() ::
{:@, [context: Exdantic.Schema, imports: [...]],
[{:model_validators, [...], [...]}]}
| {:__block__, [], [{:def, [...], [...]} | {:@, [...], [...]}]}
Functions
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
@spec computed_field(atom(), term(), atom()) :: macro_ast()
@spec computed_field(atom(), term(), (map() -> {: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 functionopts
- 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:
- Field validation
- 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]
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
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
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
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
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
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
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 metadatavalue
- 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"}
@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 nametype
- 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)
- A built-in type (
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
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
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
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
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
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
@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
@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
@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
@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
@spec model_validator((map() -> {: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 implicitinput
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
@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
@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
Defines a new schema with optional description.
Parameters
description
- Optional string describing the schema's purposedo
- 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
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
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