A state validation solution with declarative composition and easy reuse.


Swift Tests GitHub release codecov

A flexible state validation solution.


  • Function builder composition API
  • Easy composition of small validators into more complex ones.
  • Easily extensible
  • Documented here


Creating validators

Create simple validators from predicates

let validateEmail = Verify<String>.that({ $0.contains("@") }, otherwise: .myError)

Extend and reuse validators

You can easily create validators on any type via extensions:

extension Verify where Subject == Int {
    public static func greaterThanZero(otherwise error: Error) -> Validator_<Subject> {
        Verify<Int>.that({ $0  >  0}, otherwise: error)

extension Verify where Subject == String {
    public static func minLength(_ value: Int, otherwise error: Error) -> Validator_<Subject> {
    Verify.that({ (string: String) in string.count >= value }, otherwise: error)

Having created these extensions they will become avaiable like this:

Verify<String>.minLength(10, otherwise: .myError)
Verify<Int>.greaterThanZero(otherwise: .myOtherError)


Verify has two flavors of composition, a senquenced or in order composition, or a parallel composition.

Sequenced composition

In sequence composition only one a validator is ran at a time and will accumulate at most one error since the next validator in the chain will only be applied when the previous succeeds.

let emailValidator = Verify<String>.inOrder {
    Verify.that({ $0.contains("@")}, otherwise: invalidEmail)
    Verify.minLength(5, otherwise: invalidEmail)

let input = "1"
emailValidator.errors(input).count == 1

Notice that even the input "1" fails both the validations only one error will be accumulated. This is usually the desired behavour since we want to validate one condition at a time.

Also can be written as:

let emailValidator = Verify<String>
    .that({ $0.contains("@")}, otherwise: invalidEmail)
    .andThat({ $0.count >= 5}, otherwise: invalidEmail)

let input = "1"
emailValidator.errors(input).count == 1

Parallel composition

In parallel composition we run all validators at once and accumulate all errors.

let emailValidator = Verify<String>.atOnce {
    Verify.that({ $0.contains("@")}, otherwise: invalidEmail)
    Verify.minLength(5, otherwise: invalidEmail)

let input = "1"
emailValidator.errors(input).count == 2

The previous example will acumulate both errors.

Cheat sheet


Method Signature Description
Verify.that (Predicate<S>) -> Validator<S> Validates with predicate (KeyPath<S, P>, Predicate<P>) -> Validator<S> Validates the property focused by keypath
Verify.error (Error) -> Validator<S> Always fails with specified error


Method Signature Accumulates errors
andThen (Validator<S>) -> Validator<S> No
andThat / thenCheck (Predicate) -> Validator No
add (Validator<S>) -> Validator<S> Yes
addCheck (Predicate<S>) -> Validator<S> Yes


Method Signature Description
ignore (Predicate<S>) -> Validator<S> Bypass validator when the provided predicate holds true


Field validations

Given a model, for instance a UserRegistration struct:

struct UserRegistration {
    let email: String
    let password: String
    let passwordConfirmation: String

we can apply validation to specific properties with keypaths.

let invalidEmail = UserRegistrationError.invalidEmail
let invalidPassword = UserRegistrationError.invalidPassword

let emailValidator = Verify<String>.inOrder {
    Verify.minLength(5, otherwise: invalidEmail)
    Verify.that({ $0.contains("@")}, otherwise: invalidEmail)

let password = Verify<String>.inOrder {
    Verify<String>.that({ $0.count > 5}, otherwise: invalidPassword)
    Verify.containsSomeOf(CharacterSet.symbols, otherwise: invalidPassword)

let registrationValidator = Verify<UserRegistration>.atOnce {
    Verify<UserRegistration>.at(\.email, validator: emailValidator)
    Verify<UserRegistration>.at(\.password, validator: password)
    Verify<UserRegistration>.that({ $0.password == $0.passwordConfirmation  }, otherwise: UserRegistrationError.passwordsDontMatch)

let errors = registrationValidator.errors(UserRegistration(email: "", password: "19d", passwordConfirmation: "12d"))

Run a validator

Running a validator is a simple as passing in a parameter since its just a function. To be a bit more eloquent a verify method is provided, this method is special because besides forwarding the argument to the calling validator it can also be used to filter the error list and have it cast to a specific error type. Just supply a specific type parameter.

Form validation

Often times you will have modeled your error type similar to:

struct FormError<FieldType>: Error {
    enum Reason {
        case invalidFormat, required

    let reason: Reason
    let field:  FieldType

enum LoginField {
    case email, password

In these scenarios its convenient to be able to group errors by field.

typealias LoginFormError = FormError<LoginField>

let validator = Verify<Int>.atOnce {
    Verify<Int>.error(LoginFormError(reason: .invalidFormat, field: .email))
    Verify<Int>.error(LoginFormError(reason: .required, field: .password))

let groupedErrors: [LoginField: [LoginFormError]] = validator.groupedErrors(0, by: { (error:  LoginFormError) in error.field })

//  Or even

let fieldErrors: [LoginField: [LoginFormError.Reason]] = groupedErrors.mapValues({  ${ $0.reason })})


  • Swift Tools 5.2.0
View More Packages from this Author


  • None
Last updated: Mon Feb 26 2024 16:49:21 GMT-1000 (Hawaii-Aleutian Standard Time)