This guide covers compile-time schema definition with Exdantic.Schema and the complete type system used by validation and JSON Schema generation.
Declaring Schemas
A schema module starts with:
defmodule UserSchema do
use Exdantic
schema "User payload" do
# fields here
end
endschema/2 accepts an optional description string and a do block with fields and configuration.
Field Declaration
Use field/2 or field/3:
field :name, :string
field :age, :integer do
optional()
gteq(0)
lteq(150)
endYou can also pass options directly:
field :title, :string, required: true
field :active, :boolean, default: true
field :meta, :string, extra: %{"source" => "api"}Field Metadata Macros
Inside a field block:
required()optional()default(value)description(text)example(value)examples(list)extra(key, value)
default/1 also marks the field optional.
Constraint Macros
Supported constraints in the schema DSL:
- String length:
min_length/1,max_length/1 - Array length:
min_items/1,max_items/1 - Numeric bounds:
gt/1,lt/1,gteq/1,lteq/1 - Regex pattern:
format/1 - Enumerated values:
choices/1
Example:
field :status, :string do
choices(["pending", "approved", "rejected"])
endBuilt-in Types
Built-in primitive types:
:string:integer:float:boolean:atom:map:any
Composite Types
Array:
field :tags, {:array, :string}Map:
field :scores, {:map, {:string, :integer}}Object (typed fixed-key map):
field :profile,
{:object,
%{
first_name: :string,
last_name: :string
}}Union:
field :id, {:union, [:string, :integer]}Tuple:
field :coordinates, {:tuple, [:float, :float]}Schema reference:
field :address, AddressSchemaLiteral atom matching is also supported by the validator path when used as a type atom.
Programmatic Types with Exdantic.Types
Exdantic.Types provides constructors for runtime composition:
alias Exdantic.Types
name_type =
Types.string()
|> Types.with_constraints(min_length: 2, max_length: 80)
email_type =
Types.string()
|> Types.with_constraints(format: ~r/@/)
|> Types.with_error_message(:format, "must be a valid email")Key helpers:
Types.string/0,Types.integer/0,Types.float/0,Types.boolean/0Types.type/1generic constructor (e.g.,Types.type(:string))Types.array/1,Types.map/2,Types.object/1,Types.union/1,Types.tuple/1Types.ref/1,Types.normalize_type/1Types.with_constraints/2Types.with_error_message/3,Types.with_error_messages/2Types.with_validator/2for custom value-level checksTypes.validate/2for direct type checking (e.g.,Types.validate(:string, value))Types.coerce/2for standalone type coercion (e.g.,Types.coerce(:integer, "123"))
Custom Type Modules with Exdantic.Type
Define reusable custom types by implementing Exdantic.Type behavior:
defmodule MyApp.Types.Email do
use Exdantic.Type
def type_definition do
{:type, :string, [format: ~r/^[^@]+@[^@]+\.[^@]+$/]}
end
def json_schema do
%{"type" => "string", "format" => "email"}
end
def validate(value) do
case Exdantic.Validator.validate(type_definition(), value, []) do
{:ok, v} -> {:ok, v}
{:error, _} -> {:error, "invalid email"}
end
end
endOptional callbacks:
coerce_rule/0— returns a coercion function or{module, function}tuple (defaultnil)custom_rules/0— returns a list of additional validation function names defined in the module (default[])
Then use it as a field type:
field :email, MyApp.Types.EmailSchema Config Block
Configure schema-level behavior:
config do
title("User")
config_description("Validates user payloads")
strict(true)
endstrict(true)forbids unknown input fieldstitleandconfig_descriptionpropagate to JSON Schema metadata
Validation Semantics to Know
- Both atom and string keys are accepted for input maps.
- Missing required fields emit
:requirederrors. - Unknown fields in strict mode emit
:additional_propertieserrors. - Constraints are enforced after base type checks.
- Custom validator constraints return
{:ok, value}or{:error, message}.
Next Guides
guides/03_structs_model_validators_computed_fields.mdguides/04_runtime_schemas.md