Packages

Bidirectional, error-accumulating validation and sanitization codecs for Gleam.

Current section

Files

Jump to
pedantic README.md
Raw

README.md

# pedantic
<p align="center">
<img src="https://raw.githubusercontent.com/gleam-foundry/pedantic/main/assets/images/pedantic-logo.png" alt="Pedantic logo" width="480">
</p>
[![Package Version](https://img.shields.io/hexpm/v/pedantic)](https://hex.pm/packages/pedantic)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/pedantic/)
Explicit codecs. Trusted data.
Error-accumulating, bidirectional validation and sanitization codecs for Gleam.
Pedantic transforms untrusted external payloads into typed domain values while
supporting Erlang and JavaScript targets.
## Contents
- [Start here](#start-here)
- [Why Pedantic](#why-pedantic)
- [Sanitization and normalization](#sanitization-and-normalization)
- [Build schemas](#build-schemas)
- [Installation](#installation)
- [Development and tests](#development-and-tests)
- [API reference](#api-reference)
- [License](#license)
## Start here
Parse a dynamic value with either coercing or exact primitive codecs:
```gleam
import pedantic
pub fn parse_values() {
// Coerced primitives accept compatible representations.
pedantic.safe_parse(pedantic.coerced_int, to_dynamic("123"))
// -> Ok(123)
pedantic.safe_parse(pedantic.coerced_bool, to_dynamic("true"))
// -> Ok(True)
// Exact primitives reject type mismatches.
pedantic.safe_parse(pedantic.exact_int, to_dynamic("123"))
// -> Error(Report(...))
}
```
For JSON request payloads, use the JSON entrypoint:
```gleam
case pedantic.safe_parse_json(user_codec(), json_payload) {
Ok(user) -> register_user(user)
Error(report) -> return_validation_errors(report)
}
```
## Why Pedantic
Pedantic is built around three ideas:
- **Accumulate, don't short-circuit**: collect nested failures in one pass with
exact paths such as `["address", "city"]`.
- **Choose coercion explicitly**: accept query-string representations when
useful, or enforce strict JSON types at the boundary.
- **Keep schemas bidirectional**: decode untrusted input into domain values and
encode those values back to JSON through the same codec.
## Sanitization and normalization
Sanitizers transform successfully decoded values before later validators run.
They make equivalent input consistent, but they are not replacements for
context-specific HTML, SQL, shell, or URL escaping.
```gleam
pub const username_codec =
pedantic.string
|> pedantic.collapse_whitespace()
|> pedantic.trim()
|> pedantic.lowercase()
|> pedantic.remove_chars("-_ ")
|> pedantic.min_length(3)
pub const optional_nickname_codec =
pedantic.string
|> pedantic.empty_as_none()
pub const normalized_email_codec =
pedantic.string
|> pedantic.normalize_email()
|> pedantic.email("Please provide a valid email address")
```
Available string sanitizers include `trim`, `collapse_whitespace`, `lowercase`,
`uppercase`, `replace`, `remove_chars`, `allow_chars`, `strip_control_chars`,
`default_if_blank`, `empty_as_none`, `normalize_email`, and `normalize_uuid`.
Additional helpers include `digits_only`, `letters_only`, `remove_whitespace`,
`normalize_line_endings`, and `strip_non_ascii`.
`replace` performs literal substring replacement rather than regular-expression
replacement. Transformations run while decoding; encoding uses the application
value.
## Build schemas
Compose constraints, sanitizers, nested objects, defaults, and custom error
messages into a bidirectional codec:
```gleam
import pedantic
import pedantic/curry
pub type Address {
Address(city: String, zip: String)
}
pub type User {
User(id: Int, name: String, address: Address, role: String)
}
fn address_codec() {
pedantic.object("Address", curry.curry2(Address))
|> pedantic.key_required(
"city",
pedantic.string |> pedantic.trim(),
fn(address: Address) { address.city },
)
|> pedantic.key_required(
"zip",
pedantic.string |> pedantic.length(5),
fn(address: Address) { address.zip },
)
|> pedantic.build()
}
pub fn user_codec() {
pedantic.object("User", curry.curry4(User))
|> pedantic.key_required(
"id",
pedantic.int |> pedantic.positive(),
fn(user: User) { user.id },
)
|> pedantic.key_required("name", pedantic.string, fn(user: User) { user.name })
|> pedantic.key_required(
"address",
address_codec(),
fn(user: User) { user.address },
)
|> pedantic.key_with_default(
"role",
pedantic.string,
"guest",
fn(user: User) { user.role },
)
|> pedantic.build()
}
```
Nested failures can be flattened for an API response:
```gleam
let issue_map = pedantic.issues_dict(report)
// {
// "id": ["Value must be at least 1"],
// "address.zip": ["Length must be exactly 5"]
// }
```
## Installation
Add Pedantic to a Gleam project:
```sh
gleam add pedantic
```
Then import the top-level API:
```gleam
import pedantic
```
## Development and tests
The library is tested on both Erlang and JavaScript runtimes:
```sh
gleam format --check
gleam build
gleam test
gleam test --target js
gleam docs build
```
## API reference
### Primitive codecs
`int`, `float`, `bool`, and `string` use coercing decoders. Use
`exact_int`, `exact_float`, `exact_bool`, and `exact_string` when coercion is
not wanted.
### Validation
Use `min`, `max`, `min_length`, `max_length`, `length`, `positive`, `negative`,
`non_negative`, `non_positive`, `email`, `uuid`, `check`, `assert_that`, and
`refine` to constrain decoded values.
### Composition
Use `list`, `optional`, `dict`, `object`, `key_required`, `key_optional`,
`key_with_default`, `build`, and `custom` to compose schemas. Use `encode_json`
to serialize a trusted value through its codec.
## License
MIT License - Copyright (c) 2026 Antonio Ognio
Made with ❤️ from 🇵🇪. El Perú es clave 🔑.