Liking cljdoc? Tell your friends :D

validator

Powered by KineticFire Labs License: Apache 2.0 Clojars Project

Streamlined data validation for Clojure, ClojureScript, and Babashka with zero dependencies, comprehensive validation functions, and clear error reporting that keeps your code readable and maintainable.

Contents

  1. Motivation
  2. Installation
  3. Usage
  4. Documentation
  5. License

Motivation

Data validation often leads to complex, deeply nested code that becomes difficult to read and maintain. Traditional validation approaches result in code that "marches to the right" with multiple nested conditionals, making it hard to understand validation logic and error handling.

The validator library solves this by providing a simple, consistent API for data validation that keeps your code linear and readable:

;; Before: Complex, nested validation
(defn validate-user [user]
  (cond
    (not (string? (:name user)))
    {:error "Name must be a string"}
    
    (< (count (:name user)) 1)
    {:error "Name cannot be empty"}
    
    (not (and (string? (:email user)) (re-find #"@" (:email user))))
    {:error "Email must be valid"}
    
    (not (and (number? (:age user)) (>= (:age user) 0)))
    {:error "Age must be a positive number"}
    
    :else
    {:valid true :data user}))

;; After: Streamlined validation with validator's run-checks
(ns my-app.validation
  (:require [kineticfire.validator.checks :as checks]
            [kineticfire.validator.runner :as runner]))

(defn validate-user [user]
  (let [result (runner/run-checks 
                 [#(checks/string-explain (:name user) {:min 1})
                  #(checks/string-explain (:email user) {:pat-or-pats-sub #"@"})
                  #(checks/number-explain (:age user) {:type :int :min 0})])]
    (if (= true result)
      {:valid true :data user}
      {:error "Validation failed" :details result})))

Key benefits:

  • Clear, consistent API across all validation functions
  • Detailed error reporting with codes, messages, and context
  • Zero dependencies - works anywhere Clojure runs
  • Cross-platform - supports Clojure, ClojureScript, and Babashka
  • Composable validation - combine simple checks into complex workflows

Installation

The validator library can be installed from Clojars using one of the following methods:

Leiningen/Boot

[com.kineticfire/validator "1.0.0"]

Clojure CLI/deps.edn

com.kineticfire/validator {:mvn/version "1.0.0"}

Gradle

implementation("com.kineticfire:validator:1.0.0")

Maven

<dependency>
  <groupId>com.kineticfire</groupId>
  <artifactId>validator</artifactId>
  <version>1.0.0</version>
</dependency>

Usage

Basic Validation

Require the main validation namespace and start validating data:

(ns my-app.core
  (:require [kineticfire.validator.checks :as checks]))

;; Boolean validation - returns true/false
(checks/string? "hello")
;;=> true

(checks/string? "hello" {:min 10})
;;=> false

(checks/number? 42 {:type :int :min 0 :max 100})
;;=> true

;; Explain validation - returns detailed results
(checks/string-explain "hi" {:min 5})
;;=> {:valid? false
;;    :code :string/too-short
;;    :message "String shorter than min length 5."
;;    :expected {:min 5}
;;    :value "hi"}

(checks/number-explain 150 {:max 100})
;;=> {:valid? false
;;    :code :number/too-large
;;    :message "Number is larger than max 100."
;;    :expected {:max 100}
;;    :value 150}

;; Collection validation
(checks/collection? [1 2 3] {:type :vec :min 2})
;;=> true

;; Map entry validation (nested key support)
(checks/map-entry? {:user {:name "Alice"}} [:user :name] {:type :string})
;;=> true

Runner Orchestration

For complex validation workflows, use the runner to orchestrate multiple validation steps:

(ns my-app.validation
  (:require [kineticfire.validator.checks :as checks]
            [kineticfire.validator.runner :as runner]))

;; Define validation steps
(def user-validation-steps
  [{:pred #(checks/string-as-keyword? (:username %))
    :code :user/invalid-username
    :msg "Username must be keyword-safe"}
   
   {:pred #(checks/string? (:email %) {:min 5})
    :code :user/invalid-email
    :msg "Email must be at least 5 characters"}
   
   {:pred #(checks/number? (:age %) {:type :int :min 13 :max 120})
    :code :user/invalid-age
    :msg "Age must be between 13 and 120"}])

;; Run validation (fail-fast mode)
(runner/explain user-data user-validation-steps)
;;=> {:valid? true :value user-data}  ; or first error

;; Run validation (collect all errors)
(runner/explain user-data user-validation-steps {:mode :all})
;;=> {:valid? false 
;;    :errors [{:code :user/invalid-email ...} {:code :user/invalid-age ...}]
;;    :value user-data}

Result Processing

Process and transform validation results for your application needs:

(ns my-app.results
  (:require [kineticfire.validator.checks :as checks]
            [kineticfire.validator.result :as result]))

(let [validation-result (checks/string-explain "test" {:min 10})]
  ;; Check if valid
  (result/valid? validation-result)
  ;;=> false
  
  ;; Collapse to simple true/error
  (result/collapse-result validation-result "Invalid input")
  ;;=> "Invalid input"
  
  ;; Extract error details
  (result/errors validation-result)
  ;;=> [{:code :string/too-short :message "String shorter than min length 10."}])

Control Flow Integration (Optional)

The clojure-control-flow library can help produce more efficient validation code that is more maintainable and readable. Using the threading macros such as continue-> with validation forms, validation code will stop at the first invalid form instead of continuing through all forms. The pattern also keep code more readable vs. marching to the right with each validation step.

Note: The clojure-control-flow library is NOT required to use the validator library, but can be helpful for managing complex validation workflows and preventing deeply nested validation code.

;; Option 1: Standard approach (works perfectly with validator)
(defn validate-registration [form-data]
  (let [username-result (checks/string-as-keyword-explain (:username form-data) {:min 3})]
    (if (:valid? username-result)
      (let [email-result (checks/string-explain (:email form-data) {:pat-or-pats-whole #"^[^@]+@[^@]+\.[^@]+$"})]
        (if (:valid? email-result)
          (let [age-result (checks/number-explain (:age form-data) {:type :int :min 13})]
            (if (:valid? age-result)
              {:valid? true :data form-data}
              age-result))
          email-result))
      username-result)))

;; Option 2: With optional clojure-control-flow (for enhanced readability)
(require '[kineticfire.control-flow :refer [continue->]])

(defn validate-registration [form-data]
  (continue-> form-data
              (validate-username)
              (validate-email) 
              (validate-password)
              (validate-age)
              (validate-terms)))

(defn validate-username [data]
  (let [result (checks/string-as-keyword-explain (:username data) {:min 3})]
    (assoc data :validation result)))

(defn validate-email [data]
  (let [result (checks/string-explain (:email data) {:pat-or-pats-whole #"^[^@]+@[^@]+\.[^@]+$"})]
    (assoc data :validation result)))

The validator library works perfectly on its own. For developers who prefer enhanced control flow, the optional integration with clojure-control-flow can help prevent "marching to the right" in complex validation scenarios.

Examples

Comprehensive examples demonstrating all library features are available in the examples/ directory:

  • Run basic validation examples: lein basic
  • Run comprehensive integration examples: lein examples
  • View example source: examples/kineticfire/validator/examples/

Documentation

Basic Checks (kineticfire.validator.checks)

The core validation functions that form the foundation of the library. Each function comes in two forms: a boolean predicate (returns true/false) and an explain variant (returns detailed result maps).

string?

(string? s)
(string? s settings)

Boolean predicate that validates a string with optional constraints.

Settings Map:

  • :nil-ok - boolean, allow nil as valid (default: false)
  • :min - integer, minimum length inclusive (optional)
  • :max - integer, maximum length inclusive (optional)
  • :pat-or-pats-whole - Pattern or collection of Patterns, all must match whole string (optional)
  • :pat-or-pats-sub - Pattern or collection of Patterns, all must match as substring (optional)
  • :fn-or-fns - function or collection of functions, each must return truthy (optional)
(string? "hello")
;;=> true

(string? "hello" {:min 3 :max 10})
;;=> true

(string? "hi" {:min 5})
;;=> false

(string? "test@example.com" {:pat-or-pats-whole #"^[^@]+@[^@]+\.[^@]+$"})
;;=> true

(string? "Hello World" {:fn-or-fns #(.startsWith % "Hello")})
;;=> true

string-explain

(string-explain s)
(string-explain s settings)

Explain-style validator for strings. Returns {:valid? true :value s} on success, or detailed error map on failure.

Error Codes:

  • :string/nil - Value is nil
  • :type/not-string - Expected string, got different type
  • :string/too-short - String shorter than minimum length
  • :string/too-long - String longer than maximum length
  • :string/regex-whole-failed - Whole-string pattern match failed
  • :string/regex-substr-failed - Substring pattern match failed
  • :string/predicate-failed - Custom predicate returned false
(string-explain "hello")
;;=> {:valid? true :value "hello"}

(string-explain "hi" {:min 5})
;;=> {:valid? false
;;    :code :string/too-short
;;    :message "String shorter than min length 5."
;;    :expected {:min 5}
;;    :value "hi"}

(string-explain 42)
;;=> {:valid? false
;;    :code :type/not-string
;;    :message "Expected string, got java.lang.Long."
;;    :value 42}

string-as-keyword?

(string-as-keyword? s)
(string-as-keyword? s settings)

Boolean predicate that validates if a string can be safely converted to a Clojure keyword. Uses the same settings as string? but enforces additional constraints: :min defaults to 1 and is clamped to at least 1, and the string must match the keyword-safe pattern ^[a-zA-Z][a-zA-Z0-9_-]*$.

(string-as-keyword? "valid-keyword")
;;=> true

(string-as-keyword? "valid_keyword_123")
;;=> true

(string-as-keyword? "123invalid")
;;=> false

(string-as-keyword? "invalid@keyword")
;;=> false

(string-as-keyword? "")
;;=> false

string-as-keyword-explain

(string-as-keyword-explain s)
(string-as-keyword-explain s settings)

Explain-style validator for keyword-safe strings. Additional error code: :string/not-keyword-safe for strings that don't match the keyword pattern.

(string-as-keyword-explain "valid-keyword")
;;=> {:valid? true :value "valid-keyword"}

(string-as-keyword-explain "123invalid")
;;=> {:valid? false
;;    :code :string/not-keyword-safe
;;    :message "Not keyword-safe: 123invalid"
;;    :expected {:regex ["^[a-zA-Z][a-zA-Z0-9_-]*$"]}
;;    :value "123invalid"}

number?

(number? n)
(number? n settings)

Boolean predicate that validates a number with optional type and range constraints.

Settings Map:

  • :nil-ok - boolean, allow nil as valid (default: false)
  • :type - keyword, one of #{:int :float :double :decimal :ratio} (optional)
  • :min - minimum value inclusive (optional)
  • :max - maximum value inclusive (optional)
  • :fn-or-fns - function or collection of functions, each must return truthy (optional)
(number? 42)
;;=> true

(number? 42 {:type :int :min 0 :max 100})
;;=> true

(number? 3.14 {:type :float})
;;=> false  ; 3.14 is a Double, not Float

(number? 150 {:max 100})
;;=> false

(number? 8 {:fn-or-fns even?})
;;=> true

number-explain

(number-explain n)
(number-explain n settings)

Explain-style validator for numbers with detailed error reporting.

Error Codes:

  • :number/nil - Value is nil
  • :type/not-number - Expected number, got different type
  • :number/wrong-type - Number is not of requested type
  • :number/too-small - Number smaller than minimum
  • :number/too-large - Number larger than maximum
  • :number/predicate-failed - Custom predicate returned false
(number-explain 42)
;;=> {:valid? true :value 42}

(number-explain 3.14 {:type :int})
;;=> {:valid? false
;;    :code :number/wrong-type
;;    :message "Number is not of requested type :int."
;;    :expected {:type :int}
;;    :value 3.14}

(number-explain "42")
;;=> {:valid? false
;;    :code :type/not-number
;;    :message "Expected number, got java.lang.String."
;;    :value "42"}

collection?

(collection? c)
(collection? c settings)

Boolean predicate that validates a collection with type, size, and content constraints.

Settings Map:

  • :nil-ok - boolean, allow nil as valid (default: false)
  • :min - integer, minimum count inclusive (optional)
  • :max - integer, maximum count inclusive (optional)
  • :type - keyword, one of #{:vec :list :set :map :seq :assoc} (optional)
  • :duplicates-ok - boolean, allow duplicate values (default: true)
  • :nil-value-ok - boolean, allow nil values within collection (default: true)
  • :fn-or-fns - function or collection of functions, each must return truthy (optional)
(collection? [1 2 3])
;;=> true

(collection? [1 2 3] {:type :vec :min 2 :max 5})
;;=> true

(collection? [1 2 2 3] {:duplicates-ok false})
;;=> false

(collection? [1 nil 3] {:nil-value-ok false})
;;=> false

(collection? #{:a :b :c} {:type :set})
;;=> true

collection-explain

(collection-explain c)
(collection-explain c settings)

Explain-style validator for collections with comprehensive error reporting.

Error Codes:

  • :collection/nil - Value is nil
  • :type/not-collection - Expected collection, got different type
  • :collection/wrong-type - Collection is not of requested type
  • :collection/too-small - Collection smaller than minimum count
  • :collection/too-large - Collection larger than maximum count
  • :collection/duplicates-found - Collection contains duplicate values
  • :collection/nil-values-found - Collection contains nil values
  • :collection/predicate-failed - Custom predicate returned false
(collection-explain [1 2 3])
;;=> {:valid? true :value [1 2 3]}

(collection-explain [1 2 3 4] {:max 3})
;;=> {:valid? false
;;    :code :collection/too-large
;;    :message "Collection larger than max count 3."
;;    :expected {:max 3}
;;    :value [1 2 3 4]}

(collection-explain "not-a-collection")
;;=> {:valid? false
;;    :code :type/not-collection
;;    :message "Expected collection, got java.lang.String."
;;    :value "not-a-collection"}

map-entry?

(map-entry? m k-or-ks)
(map-entry? m k-or-ks settings)

Boolean predicate that validates a map entry (key-value pair) with support for nested key paths and value constraints.

Parameters:

  • m - the map to validate the entry in
  • k-or-ks - single key or key sequence for navigation (e.g., :name or [:user :profile :name])

Settings Map:

  • :nil-ok - boolean, allow map to be nil (default: false)
  • :key-required - boolean, require key path to exist (default: true)
  • :nil-value-ok - boolean, allow value at key to be nil (default: false)
  • :type - keyword, one of #{:string :number :boolean :keyword :col :vec :set :map :fn} (optional)
  • :fn-or-fns - function or collection of functions, each must return truthy (optional)
(map-entry? {:name "Alice"} :name)
;;=> true

(map-entry? {:user {:profile {:name "Bob"}}} [:user :profile :name])
;;=> true

(map-entry? {:name "Alice"} :name {:type :string})
;;=> true

(map-entry? {:tags ["clojure"]} :tags {:type :vec})
;;=> true

(map-entry? {:count 42} :count {:type :number :fn-or-fns pos?})
;;=> true

map-entry-explain

(map-entry-explain m k-or-ks)
(map-entry-explain m k-or-ks settings)

Explain-style validator for map entries with detailed navigation and validation error reporting.

Error Codes:

  • :map-entry/nil-map - Map is nil
  • :map-entry/not-map - Expected map, got different type
  • :map-entry/key-not-found - Key path not found in map
  • :map-entry/nil-value - Value at key path is nil
  • :map-entry/wrong-type - Value is not of requested type
  • :map-entry/predicate-failed - Custom predicate returned false
(map-entry-explain {:name "Alice"} :name)
;;=> {:valid? true :value "Alice"}

(map-entry-explain {:name "Alice"} :missing)
;;=> {:valid? false
;;    :code :map-entry/key-not-found
;;    :message "Key path :missing not found in map."
;;    :expected {:key-path :missing}
;;    :value {:name "Alice"}}

(map-entry-explain {:name "Alice"} :name {:type :number})
;;=> {:valid? false
;;    :code :map-entry/wrong-type
;;    :message "Value is not of requested type :number."
;;    :expected {:type :number}
;;    :value {:name "Alice"}}

Runner (kineticfire.validator.runner)

The orchestration layer for executing multiple validation steps and aggregating results. Provides flexible validation workflows with different execution modes.

explain

(explain v steps)
(explain v steps opts)
(explain v steps opts defaults)

Run one or more validation steps against a value with configurable execution modes.

Parameters:

  • v - value to validate
  • steps - single step or collection of steps
  • opts - options map {:mode :first | :all, :path [...]}
  • defaults - default step fields for function-only steps

Step Forms:

;; Function step (requires defaults for error info)
#(pos? %)

;; Map step (explicit)
{:pred #(pos? %)
 :code :number/not-positive
 :msg "Number must be positive"
 :expected {:min 0}
 :when #(number? %)        ; optional guard
 :select :count            ; optional value selector
 :path [:user]             ; optional path context
 :via :validation/step-1}  ; optional step identifier
;; Simple validation
(explain 42 #(pos? %) {} {:code :invalid :msg "Failed"})
;;=> {:valid? true :value 42}

;; Multi-step validation (fail-fast mode)
(explain {:name "Alice" :age 25}
         [{:pred #(string? (:name %)) :code :name/invalid}
          {:pred #(number? (:age %)) :code :age/invalid}])
;;=> {:valid? true :value {:name "Alice" :age 25}}

;; Multi-step validation (collect all errors)  
(explain {:name 123 :age "invalid"}
         [{:pred #(string? (:name %)) :code :name/invalid}
          {:pred #(number? (:age %)) :code :age/invalid}]
         {:mode :all})
;;=> {:valid? false
;;    :errors [{:valid? false :code :name/invalid ...}
;;             {:valid? false :code :age/invalid ...}]
;;    :value {:name 123 :age "invalid"}}

run-all

(run-all v steps)
(run-all v steps defaults)

Convenience function equivalent to (explain v steps {:mode :all} defaults). Collects all validation errors rather than stopping at the first failure.

(run-all user-data validation-steps)
;;=> {:valid? false :errors [...] :value user-data}

run-first

(run-first v steps)
(run-first v steps defaults)

Convenience function equivalent to (explain v steps {:mode :first} defaults). Stops at the first validation failure (fail-fast mode).

(run-first user-data validation-steps)
;;=> {:valid? false :code ... :message ... :value user-data}

run-checks

(run-checks check-fns)

Execute a collection of validation functions in fail-fast mode. Takes 0-arity functions that return validation results and stops at the first failure, returning either true (all pass) or the first failure result.

Parameters:

  • check-fns - collection of 0-arity functions that return:
    • Boolean values (true/false)
    • Explain-style maps ({:valid? true/false ...})

Return Values:

  • true if all validations pass
  • First failure result (boolean false or explain-style error map)
  • Error map if empty collection provided ({:valid? false :code :checks/no-functions ...})

Key Features:

  • Fail-fast execution - stops at first failure for efficiency
  • Mixed validation types - supports both boolean and explain-style validators
  • Streamlined syntax - clean, readable validation chains
  • Comprehensive error handling - clear errors for edge cases
;; All validations pass
(run-checks [#(checks/string? "test" {:min 1})
             #(checks/number? 42 {:type :int :min 0})
             #(checks/collection? [1 2 3] {:type :vec})])
;;=> true

;; First validation fails (boolean)
(run-checks [#(checks/string? "test" {:min 10})      ; false - too short
             #(checks/number? 42 {:type :int})])     ; would be true
;;=> false

;; First validation fails (explain-style)
(run-checks [#(checks/string-explain "" {:min 1})    ; explain-style failure
             #(checks/number-explain 42)])           ; would be valid
;;=> {:valid? false
;;    :code :string/too-short
;;    :message "String shorter than min length 1."
;;    :expected {:min 1}
;;    :value ""}

;; Mixed boolean and explain-style validations
(run-checks [#(checks/string? "test" {:min 1})       ; boolean true
             #(checks/number-explain 42 {:type :int}) ; explain-style valid
             #(checks/collection? [1 2 3])])         ; boolean true
;;=> true

;; User registration example
(defn validate-user [user]
  (runner/run-checks 
    [#(checks/string-as-keyword-explain (:username user) {:min 3})
     #(checks/string-explain (:email user) {:min 5 :pat-or-pats-sub #"@"})
     #(checks/number-explain (:age user) {:type :int :min 13 :max 120})]))

(validate-user {:username "john_doe" :email "john@example.com" :age 25})
;;=> true

(validate-user {:username "" :email "bad" :age 5})
;;=> {:valid? false :code :string/too-short ...}  ; first failure

;; Edge case: empty validation list
(run-checks [])
;;=> {:valid? false :code :checks/no-functions :message "No validation functions provided." :value []}

Result Processing (kineticfire.validator.result)

Utilities for interpreting, transforming, and combining validation results into formats suitable for your application.

collapse-result

(collapse-result result err)

Collapse an explain map into true on success, or return the provided error value on failure.

(collapse-result {:valid? true :value "test"} "Invalid")
;;=> true

(collapse-result {:valid? false :code :error} "Invalid")
;;=> "Invalid"

collapse-results

(collapse-results results err)

Collapse a sequence of explain maps. Returns true if all are valid, otherwise returns the provided error value.

(collapse-results [{:valid? true} {:valid? true}] "Invalid")
;;=> true

(collapse-results [{:valid? true} {:valid? false}] "Invalid")  
;;=> "Invalid"

combine-results

(combine-results results)

Combine multiple explain results into a single aggregated map with overall validity and collected errors.

(combine-results [{:valid? true :value "a"}
                  {:valid? false :code :error :message "Failed"}])
;;=> {:valid? false
;;    :errors [{:code :error :message "Failed"}]}

valid?

(valid? result)

Shorthand to check validity from an explain result. Always returns a boolean.

(valid? {:valid? true})
;;=> true

(valid? {:valid? false})
;;=> false

(valid? nil)
;;=> false

errors

(errors result-or-results)

Extract error details (codes and messages) from an explain result or collection of results.

(errors {:valid? false :code :test/failed :message "Test failed"})
;;=> [{:code :test/failed :message "Test failed"}]

(errors [{:valid? true} {:valid? false :code :error :message "Failed"}])
;;=> [{:code :error :message "Failed"}]

Pattern Utilities (kineticfire.validator.patterns)

Internal utilities for regex pattern matching used by the string validation functions. These are primarily for advanced use cases and internal library operations.

The pattern utilities provide helper functions for matching regular expressions against strings in both whole-string and substring modes, supporting the flexible pattern matching capabilities used by string? and string-explain.

License

The validator project is released under Apache License 2.0

Can you improve this documentation? These fine people already did:
Kris Hall & kineticfire-labs
Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close