Packages

Lightweight Erlang validator (erlang_standard default, LIVR opt-in)

Current section

Files

Jump to
liver README.md
Raw

README.md

# Liver
[![Build Status](https://github.com/erlangbureau/liver/actions/workflows/ci.yml/badge.svg)](https://github.com/erlangbureau/liver/actions)
[![Coverage Status](https://coveralls.io/repos/github/erlangbureau/liver/badge.svg?branch=master)](https://coveralls.io/github/erlangbureau/liver?branch=master)
## Summary
Liver is a lightweight Erlang/OTP data validator **inspired by
[LIVR](http://livr-spec.org)**. It follows the same ideas: declarative rules per
field, all errors at once, stable error codes, and easy custom rules.
Liver ships **two rule sets**:
- **`erlang_standard`** (default) — for Erlang terms inside OTP applications
- **`livr_spec`** — full LIVR 2.0 rule names and behaviour (including the
upstream LIVR test suite)
Both are first-class. The default changed in 1.0.0 because most Erlang code
works with typed terms, not JSON strings; LIVR remains fully available via
`#{rule_set => livr_spec}` (or `livr_compatible => true`).
| Docs | |
|------|--|
| [Standard rules](doc/standard_rules.md) | Reference for `erlang_standard` |
| [LIVR rules](doc/livr_rules.md) | Reference for `livr_spec` |
| [Comparing the sets](doc/livr_vs_standard.md) | Coercion, naming, when to use which |
| [OpenAPI](doc/openapi.md) | Export / import Schema Objects (MVP) |
| [Changelog](CHANGELOG.md) | Releases and breaking changes |
## Table of Contents
* [Description](#description)
* [Getting Started](#getting-started)
* [Usage Examples](#usage-examples)
* [Rule sets](#rule-sets)
* [OpenAPI](#openapi)
* [Exports](#exports)
* [License](#license)
## Description
**From LIVR (design shared by Liver):**
1. Declarative rules, many per field
2. All field errors returned together
3. Fields without rules are excluded from the output
4. Nested structures supported
5. Stable error codes (not free-form messages)
6. Easy to add project-specific rules
7. Rules may transform values (`trim`, nested validators, converters, …)
8. Unicode-aware where it matters
**Liver extras:**
1. Two built-in rule maps — LIVR-spec and Erlang-oriented — selectable / composable
2. Maps and proplists as input/output (`return => map | proplist | as_is`)
3. `strict` option — reject fields not described in the schema
4. List as root value
5. `add_rule/2`, `add_rule_set/2`, custom error messages
### Why two rule sets?
LIVR grew up around **JSON and HTML forms**: numbers and booleans often arrive
as **binaries** (`<<"10">>`, `<<"true">>`). Spec rules such as `integer` therefore
**parse and coerce** as part of validation.
OTP applications more often already have **Erlang types** (`10`, `true`, atoms).
Silent coercion there hides bugs. The default `erlang_standard` set uses
predicates (`is_integer`, …) and **explicit** `to_*` converters when you want
parsing.
See [Comparing the sets](doc/livr_vs_standard.md) for a fuller explanation.
## Getting Started
1. Add as a dependency:
* **Hex** — `rebar.config`:
```erl
{deps, [{liver, "1.1.0"}]}.
```
* **Hex** — erlang.mk:
```make
DEPS = liver
dep_liver = hex 1.1.0
```
* **Git** (tag) — `rebar.config`:
```erl
{deps, [
{liver, {git, "https://github.com/erlangbureau/liver.git", {tag, "1.1.0"}}}
]}.
```
* **Git** (tag) — erlang.mk:
```make
DEPS = liver
dep_liver = git https://github.com/erlangbureau/liver.git 1.1.0
```
2. Add `liver` to `applications` in your `.app.src`.
3. Validate data, or register your own rules with `liver:add_rule/2`.
> **Upgrading from 0.9.x:** Liver was LIVR-oriented from the first version
> (2017-11-21). In **1.0.0** the default rule set became `erlang_standard`.
> Existing LIVR schemas keep working with
> `#{rule_set => livr_spec}` or `#{livr_compatible => true}`.
> Details: [CHANGELOG.md](CHANGELOG.md#100---2026-10-03).
>
> **Upgrading to 1.1.0:** `erlang_standard` errors are lowercase atoms
> (`not_integer`) instead of binaries (`<<"NOT_INTEGER">>`). `livr_spec` is
> unchanged. Details: [CHANGELOG.md](CHANGELOG.md#110---2026-10-07).
## Usage Examples
### Erlang-oriented rules (default)
```erlang
1> Schema = #{
name => [required, is_utf8_binary],
age => [required, is_pos_integer],
role => [{one_of_terms, [[admin, user]]}]
}.
2> liver:validate(Schema, #{name => <<"Ann">>, age => 30, role => admin}).
{ok,#{age => 30,name => <<"Ann">>,role => admin}}
3> %% Binary is not an integer — no silent parse
3> liver:validate(#{n => is_integer}, #{n => <<"10">>}).
{error,#{n => not_integer}}
4> %% Parse explicitly, then check
4> liver:validate(#{n => [to_integer, is_pos_integer]}, #{n => <<"10">>}).
{ok,#{n => 10}}
```
### Nested map
```erlang
5> Schema = #{
address => [required, {nested_map, #{
country => [required, is_utf8_binary],
zip => is_pos_integer
}}]
}.
6> liver:validate(Schema, #{
address => #{country => <<"UA">>, zip => 12345, extra => ignored}
}).
{ok,#{address => #{country => <<"UA">>,zip => 12345}}}
```
### Unknown fields (`strict`)
```erlang
7> liver:validate(#{a => required}, #{a => 1, b => 2}, #{strict => true}).
{error,#{b => <<"UNKNOWN_FIELD">>}}
```
### LIVR rules (same validator, LIVR rule map)
```erlang
8> Schema = #{
<<"zip">> => [required, positive_integer],
<<"street">> => [required, string]
}.
9> liver:validate(Schema, #{
<<"zip">> => <<"12345">>,
<<"street">> => <<"Main">>
}, #{rule_set => livr_spec}).
{ok,#{<<"street">> => <<"Main">>,<<"zip">> => 12345}}
```
`positive_integer` here accepts `<<"12345">>` because that is how LIVR is
specified for JSON-style input.
## Rule sets
| `rule_set` | Behaviour |
|------------|-----------|
| `erlang_standard` (default) | `liver_standard_rules` |
| `livr_spec` | `liver_livr_rules` (LIVR 2.0 names) |
| `[Set1, Set2, …]` | Compose; **first wins** on the same rule name |
| `#{Rule => Module}` | Inline custom rule map |
| `{mixed, erlang_standard}` | Alias for `[erlang_standard, livr_spec]` |
| `{mixed, livr_spec}` | Alias for `[livr_spec, erlang_standard]` |
```erlang
liver:add_rule_set(my_app, #{slug => my_app_rules}).
liver:validate(Schema, Data,
#{rule_set => [my_app, erlang_standard, livr_spec]}).
```
`#{livr_compatible => true}` is an alias for `#{rule_set => livr_spec}`.
References: [standard rules](doc/standard_rules.md), [LIVR rules](doc/livr_rules.md),
[comparison](doc/livr_vs_standard.md).
## OpenAPI
MVP helpers in `liver_openapi_schema`:
* **Export** — path map or `Module:liver_schema/0` → OpenAPI 3 document
* **Import** — Schema Object → `erlang_standard` field schema
See [doc/openapi.md](doc/openapi.md).
## Exports
### `validate/2`
```erlang
validate(Schema, Input) -> {ok, Output} | {error, Errors}
Schema, Input, Output, Errors = map() | proplist()
```
Equivalent to `validate(Schema, Input, #{})`.
### `validate/3`
```erlang
validate(Schema, Input, Opts) -> {ok, Output} | {error, Errors}
Opts = map() | proplist()
```
| Option | Default | Description |
|--------|---------|-------------|
| `return` | `as_is` | `as_is` \| `map` \| `proplist` |
| `strict` | `false` | Reject fields not in schema |
| `rule_set` | `erlang_standard` | See [Rule sets](#rule-sets) |
| `livr_compatible` | `false` | Alias for `rule_set => livr_spec` |
### `which/1`, `which/2`
```erlang
which(Rule) -> module() | undefined_module
which(Rule, Opts) -> module() | undefined_module
```
### `add_rule/2`
```erlang
add_rule(Rule, Module) -> ok
```
Register a custom rule into the `erlang_standard` application rule map
(also used when that set appears in a `rule_set` list).
### `add_rule_set/2`
```erlang
add_rule_set(Name, Rules) -> ok
Name = atom()
Rules = #{atom() => module()}
```
### `custom_error/2`
```erlang
custom_error(ErrorCode, ErrorMessage) -> ok
```
Override a built-in error code. From **1.1.0**, default `erlang_standard`
errors are lowercase atoms (`not_integer`); `livr_spec` uses LIVR binaries
(`<<"NOT_INTEGER">>`).
## License
Liver is released under the MIT License