Valpa
View SourceValpa is a composable validation library for Elixir. It works with raw values, {:ok, _}
, or {:error, _}
tuples in pipelines. It offers pipelined field validation, automatic error propagation, and structured error reporting.
Valpa provides simple, reusable validation functions for individual values or relationships between fields in a map or struct.
Why?
- Pipeline-friendly — validate values,
{:ok, _}
, or{:error, _}
directly in Elixir pipelines. - No schemas required — works with plain maps, structs, or raw values.
- Optional (
maybe_
) and required variants for all validators. - Built-in validators for numbers, strings, booleans, lists, maps, and more.
- List and map content checks — uniqueness, value sets, key inclusion/exclusion.
- Custom validators — easily extend with your own rules.
- Detailed errors — structured output with optional stacktrace for debugging.
- Predicate functions — standalone checks returning
true
orfalse
.
Installation
Add :valpa
to your mix.exs
dependencies:
def deps do
[
{:valpa, "~> 0.1.1"}
]
end
Then run:
mix deps.get
Usage
Let’s say you need to validate a person struct:
defmodule Person do
defstruct [
:name, :age, :height, :money, :has_hat, :won, :lose,
:dice_rolls, :hat_color, :car, :bike, :school, :work
]
def validate(p) do
p
|> Valpa.string(:name)
|> Valpa.integer(:age)
|> Valpa.maybe_float(:height)
|> Valpa.decimal(:money)
|> Valpa.boolean(:has_hat)
|> Valpa.integer(:won)
|> Valpa.integer(:lose)
|> Valpa.map_compare_int_keys({:>, :won, :lose})
|> Valpa.list_of_type(:dice_rolls, :integer)
|> Valpa.value_of_values(:hat_color, [:RED, :GREEN, :BLUE])
|> Valpa.maybe_value_or_uniq_list_of_values(:car, [:BMW, :AUDI, :FORD])
|> Valpa.maybe_uniq_list_of_type(:bike, :string)
|> Valpa.map_inclusive_keys([:car, :bike])
|> Valpa.maybe_string(:school)
|> Valpa.maybe_string(:work)
|> Valpa.map_exclusive_keys([:school, :work])
end
end
Valid input:
defmodule Bernard do
def create do
%Person{
name: "Bernard",
age: 34,
height: 183.5,
money: Decimal.new("53.8"),
has_hat: true,
won: 5,
lose: 3,
dice_rolls: [1, 4, 4, 5, 2, 3],
hat_color: :GREEN,
car: :FORD,
bike: ["Old", "Electric"],
school: "MIT"
}
end
end
Bernard.create() |> Person.validate()
# => {:ok, %Person{...}}
Invalid input (wrong type):
defmodule InvalidBernard do
def create do
%Person{age: "34", name: "Bernard", ...}
end
end
InvalidBernard.create() |> Person.validate()
# => {:error, %Valpa.Error{validator: :integer, value: "34", field: :age, ...}}
Invalid input (field relationship):
defmodule AnotherInvalidBernard do
def create do
%Person{won: 5, lose: 11, ...}
end
end
AnotherInvalidBernard.create() |> Person.validate()
# => {:error, %Valpa.Error{validator: :map_compare_int_keys, criteria: {:>, :won, :lose}, ...}}
Optional vs Required
Validators come in two variants:
Valpa.integer/2
— requiredValpa.maybe_integer/2
— optional (passes onnil
)
Also available for types: string
, float
, decimal
, boolean
, list_of_type
, value_of_values
, etc.
Custom Validators
Valpa supports custom validation in two ways:
- Module-based validation via
Valpa.Custom.validator
- Function-based validation via
Valpa.Custom.validate
Option 1: Custom validator module (on field)
defmodule DiceRolls do
@behaviour Valpa.CustomValidator
def validate(value) do
if Enum.sum(value) == 20, do: :ok, else: {:error, Valpa.Error.new(...) }
end
end
# In validation:
# ...
|> Valpa.Custom.validator(:dice_rolls, DiceRolls)
Option 2: Custom validator module (on full struct)
defmodule WonLose do
@behaviour Valpa.CustomValidator
def validate(%{won: won, lose: lose}) do
if won + lose == 10, do: :ok, else: {:error, Valpa.Error.new(...) }
end
end
# ...
|> Valpa.Custom.validator(WonLose)
Option 3: Inline validation function
defmodule FieldsSumEqualsTen do
def validate(data, a, b) do
if Map.get(data, a) + Map.get(data, b) == 10, do: :ok, else: {:error, Valpa.Error.new(...) }
end
end
# ...
|> Valpa.Custom.validate(&FieldsSumEqualsTen.validate(&1, :age, :won))
Error Struct
Errors are returned as %Valpa.Error{}
with fields:
:validator
— name of the validator:value
— the invalid value (or whole struct for relationship checks):field
— field being validated (if applicable):criteria
— criteria info like{:>, :a, :b}
or%{min: 0}
:text
— optional message (useful for custom validators):__trace__
— stacktrace, shown only in dev/test
See
Valpa.Error
for full structure and how to build custom errors.
Stacktrace (Quick Info)
Valpa errors can include stacktraces for debugging.
- Dev/Test: stacktraces included
- Prod: stacktraces hidden
Override defaults (optional):
config :valpa, :stacktrace, true # force stacktraces
config :valpa, :stacktrace, false # hide stacktraces
⚠️ Safe defaults applied automatically — you usually don’t need to change anything.
Predicate Functions
All built-in validators in Valpa are based on simple predicate functions defined in Valpa.Predicate.Validator
. These functions return true
or false
, making them useful on their own when you don’t need full validation:
Valpa.Predicate.Validator.integer(5)
# => true
Valpa.Predicate.Validator.integer("not a number")
# => false
Documentation
Full API docs: https://hexdocs.pm/valpa
Contributing
Contributions are welcome via issues or pull requests. Created and maintained by Centib.
License
MIT License. See LICENSE.md.