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
- Why Pedantic
- Sanitization and normalization
- Build schemas
- Installation
- Development and tests
- API reference
- License
Start here
Parse a dynamic value with either coercing or exact primitive codecs:
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:
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.
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:
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:
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:
gleam add pedantic
Then import the top-level API:
import pedantic
Development and tests
The library is tested on both Erlang and JavaScript runtimes:
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 🔑.