pedantic

Pedantic logo

Package Version Hex 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 🔑.

Search Document