Overview and Quickstart

Copy Markdown View Source

This guide introduces Exdantic's architecture and gives a practical path from first schema to advanced workflows.

What Exdantic Solves

Exdantic provides a single data-contract stack in Elixir for:

  • Schema definition with field constraints
  • Structured validation with typed error paths
  • Cross-field logic and transformations
  • Derived fields
  • Runtime schema generation
  • JSON Schema export and optimization
  • Environment-driven settings loading

Core Building Blocks

Exdantic has three complementary layers:

  1. Compile-time schema modules
  • use Exdantic
  • Best for stable contracts and shared domain models
  • Supports model validators, computed fields, and optional struct output
  1. Runtime schemas
  1. Type-centric validation

First Schema

defmodule AccountSchema do
  use Exdantic, define_struct: true

  schema "Account payload" do
    field :name, :string do
      required()
      min_length(2)
      max_length(80)
    end

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

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

    config do
      title("Account")
      strict(true)
    end
  end
end

Validate data:

{:ok, account} = AccountSchema.validate(%{
  name: "Jane",
  email: "jane@example.com"
})

# account is %AccountSchema{} because define_struct: true

Raise on failure:

account = AccountSchema.validate!(%{name: "Jane", email: "jane@example.com"})

Serialize struct back to map:

{:ok, payload} = AccountSchema.dump(account)

Error Model

Validation errors use Exdantic.Error:

  • path: nested location of the failure
  • code: machine-friendly error code
  • message: readable description

For validate!/1, Exdantic raises Exdantic.ValidationError containing all errors.

Runtime Quickstart

fields = [
  {:answer, :string, [required: true]},
  {:confidence, :float, [required: true, gteq: 0.0, lteq: 1.0]}
]

schema = Exdantic.Runtime.create_schema(fields, title: "LLM Result", strict: true)

{:ok, result} = Exdantic.Runtime.validate(%{answer: "42", confidence: 0.95}, schema)

TypeAdapter Quickstart

{:ok, 42} = Exdantic.TypeAdapter.validate(:integer, "42", coerce: true)
{:ok, ["a", "b"]} = Exdantic.TypeAdapter.validate({:array, :string}, ["a", "b"])

JSON Schema Quickstart

Compile-time schema:

schema = AccountSchema.json_schema()

Type spec:

schema = Exdantic.TypeAdapter.json_schema({:array, :integer})

Resolve references or enforce provider requirements:

resolved = Exdantic.JsonSchema.Resolver.resolve_references(schema)
openai = Exdantic.JsonSchema.Resolver.enforce_structured_output(schema, provider: :openai)

Choosing the Right API

Use compile-time schema modules when:

  • Contract is stable and shared across codebase
  • You need full DSL expressiveness
  • You want struct output and introspection

Use runtime schemas when:

  • Fields are discovered at runtime
  • You need programmatic schema assembly
  • You still want map-based validation + JSON Schema generation

Use TypeAdapter when:

  • You validate isolated values or fragments
  • You want minimal surface area and low ceremony

Next Guides

  • guides/02_schema_dsl_and_types.md: field DSL, constraints, and type system
  • guides/03_structs_model_validators_computed_fields.md: full validation pipeline behavior
  • guides/04_runtime_schemas.md: dynamic schema creation and enhanced runtime pipeline