License: MIT Hex.pm Documentation

Valpa 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 or false.

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:

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.