Schema-first DSL
Declarative helpers for strings, numbers, booleans, objects, arrays, dates, and conditionals.
v0.8 · Error tree + translations
Schema-first DSL. Convert mode. Conditional rules. Custom validators. All packaged with an extendable runtime so your product teams can ship cohesive validations everywhere.
Overview
Declarative helpers for strings, numbers, booleans, objects, arrays, dates, and conditionals.
Toggle convert mode per validation and register custom validators via ExJoi.extend/2.
Consume flattened, path-based errors and global translators for localized messaging.
Deep Guides
Prefer multi-page docs? Jump into these standalone guides.
Installation, schema scaffolding, workflows, and CLI commands.
Type matrix, casting strategies, and safety tips.
Everything about ExJoi.when/3, from simple cases to nested branches.
Register functions or modules, share plugins, and shape error builders.
Consume errors_flat, translate copy, and integrate with your API.
Live schema demo with convert toggle, conditional logic, and JSON sandbox.
Quick Start
defp deps do
[
{:exjoi, "~> 0.8.0"}
]
end
schema =
ExJoi.schema(%{
role: ExJoi.string(required: true),
friends: ExJoi.array(of: ExJoi.string(min: 3), min_items: 1, unique: true),
permissions:
ExJoi.when(
:role,
is: "admin",
then: ExJoi.array(of: ExJoi.string(), min_items: 1, required: true),
otherwise: ExJoi.array(of: ExJoi.string())
)
})
Error Tree
Use the new errors_flat map for dotted paths (e.g., user.email) and plug a translator for localized copy.
{:error,
%{
errors_flat: %{
"user.email" => ["must be a valid email"],
"permissions.0" => ["must be at least 3 characters"]
}
}}
ExJoi.configure(
message_translator: fn
:required, _default, _meta -> "es requerido"
_code, default, _meta -> default
end
)
Convert Mode
Pass convert: true to coerce strings into numbers, booleans, arrays (via delimiters), and ISO dates. Default strict mode keeps types honest.
params = %{"age" => "42", "active" => "true", "onboarded_at" => "2025-01-01T00:00:00Z"}
ExJoi.validate(params, schema, convert: true)
# {:ok, %{"age" => 42, "active" => true, "onboarded_at" => ~U[2025-01-01 00:00:00Z]}}
Conditional Rules
ExJoi.when/3Switch validators based on another field’s value, range, or regex pattern.
guardian_contact:
ExJoi.when(
:age,
max: 17,
then: ExJoi.string(required: true, min: 5)
)
Use `:matches` for plan tiers, locales, or SKU prefixes.
pro_feature_flag:
ExJoi.when(
:plan,
matches: ~r/^pro/i,
then: ExJoi.boolean(required: true),
otherwise: ExJoi.boolean()
)
Custom Validators
ExJoi.extend(:uuid, fn value ->
if Regex.match?(~r/^[0-9a-f-]{32}$/i, value) do
{:ok, String.downcase(value)}
else
{:error, [%{code: :uuid, message: "must be a UUID"}]}
end
end)
schema = ExJoi.schema(%{id: ExJoi.custom(:uuid, required: true)})
Implement ExJoi.CustomValidator to reuse logic.
defmodule MyApp.UUIDValidator do
@behaviour ExJoi.CustomValidator
def validate(value, _rule, _ctx) do
case Ecto.UUID.cast(value) do
{:ok, uuid} -> {:ok, uuid}
:error -> {:error, [%{code: :uuid, message: "invalid UUID"}]}
end
end
end
Live Playground
Toggle role and permissions to preview which branch of ExJoi.when/3 activates. This is a conceptual helper—hook it into your own validation pipeline.
Result
// Change inputs and click “Simulate Validation”
Roadmap
| Version | Status | Highlights |
|---|---|---|
| 8 (current) | Shipped | Path-based error tree, translator hooks, enhanced builder |
| 7 | Shipped | Custom validators, plugin registry, error builder overrides |
| 6 | Shipped | Conditional rules with ExJoi.when/3 |
| 5 | Shipped | Convert mode (numbers, booleans, dates), ExJoi.date/1 |
| 4 | Shipped | Array validation (length, uniqueness, delimiter coercion) |
| Next | In design | Async / parallel validation, macro DSL, performance improvements |