sift

Package Version Hex Docs

Schema validation for Gleam — constraints, error accumulation, and field paths.

All errors are collected in a single pass using the use pattern. No short-circuiting, no missing fields.

gleam add sift

Quick example

import sift
import sift/string as s
import sift/int as i

pub type UserInput {
  UserInput(name: String, email: String, age: Int)
}

pub type User {
  User(name: String, email: String, age: Int)
}

pub fn validate(input: UserInput) -> Result(User, List(sift.FieldError(String))) {
  sift.validate({
    use name <- sift.check("name", input.name, s.min_length(1, "required"))
    use email <- sift.check("email", input.email, s.email("invalid email"))
    use age <- sift.check("age", input.age, i.between(0, 150, "out of range"))
    sift.ok(User(name:, email:, age:))
  })
}

Invalid input returns all errors at once:

validate(UserInput(name: "", email: "nope", age: -1))
// -> Error([
//   FieldError(path: ["name"], error: "required"),
//   FieldError(path: ["email"], error: "invalid email"),
//   FieldError(path: ["age"], error: "out of range"),
// ])

Upgrading from 0.1.x

Errors used to always be text. Now you choose what an error is — text, an error code, an i18n key, whatever your app needs.

If you want to keep using text, there are two edits.

1. Say String in your return type.

// before
pub fn validate(input: UserInput) -> Result(User, List(sift.FieldError))

// after
pub fn validate(input: UserInput) -> Result(User, List(sift.FieldError(String)))

2. Read .error instead of .message.

// before
errors |> list.map(fn(e) { e.message })

// after
errors |> list.map(fn(e) { e.error })

That is the whole migration. Everything else — your use chains, check, check_all, nested, ok, validate — stays exactly as it was.

What you get

Errors can now be your own type, and it flows through every validator and every field path:

pub type Problem {
  Required
  TooShort(min: Int)
  NotAnEmail
}

pub fn validate(input: UserInput) -> Result(User, List(sift.FieldError(Problem))) {
  sift.validate({
    use name <- sift.check("name", input.name, s.non_empty(Required))
    use email <- sift.check("email", input.email, s.email(NotAnEmail))
    use age <- sift.check("age", input.age, i.min(13, TooShort(13)))
    sift.ok(User(name:, email:, age:))
  })
}

validate(UserInput(name: "", email: "nope", age: 9))
// -> Error([
//   FieldError(path: ["name"], error: Required),
//   FieldError(path: ["email"], error: NotAnEmail),
//   FieldError(path: ["age"], error: TooShort(13)),
// ])

The built-in validators work with any error type — s.non_empty(Required) is the same function as s.non_empty("required").

Realistic example

For a full walkthrough — nested structs, optional fields, conditional validation with when, multiple validators per field with check_all, and per-item list validation with each — see example/contacts/.

Features

Modules

ModuleValidators
siftcheck, check_all, check_optional, check_parse, check_each, check2, nested, each, refine, ok, validate, and, or, not, when, equals, custom
sift/stringmin_length, max_length, length, non_empty, matches, one_of, starts_with, ends_with, contains, email, url, uuid, numeric, alpha, alphanumeric, trimmed
sift/intmin, max, between, positive, non_negative, negative, one_of, divisible_by
sift/floatmin, max, between, positive, non_negative
sift/listmin_length, max_length, non_empty
sift/optionrequired, optional

Further documentation can be found at https://hexdocs.pm/sift.

Search Document