pedantic

Pedantic logo

Package VersionHex Docs

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

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:

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 🔑.