sema-validate

0.1.0

Schema validation for maps and data structures

$ sema pkg install sema-validate
Readme Versions 1 Dependencies 0

sema-validate

Schema validation for maps and data structures.

Install

sema pkg add sema-validate

Quick start

(import "sema-validate")

(define user-schema
  (validate/schema
    {:name     (validate/required string?)
     :age      (validate/required (validate/all-of number? (validate/in-range 0 150)))
     :email    (validate/required (validate/matches #"^.+@.+\..+$"))
     :role     (validate/required (validate/one-of "admin" "user" "guest"))
     :nickname (validate/optional string?)}))

(validate/valid? user-schema
  {:name "Alice" :age 30 :email "alice@example.com" :role "admin"})
; => #t

(validate/errors user-schema {:name 42 :age -5 :role "superadmin"})
; => ({:message "expected all-of: number?, in-range 0 150" :path (:age) :value -5}
;     {:message "required field missing" :path (:email) :value nil}
;     {:message "expected string?" :path (:name) :value 42}
;     {:message "expected one-of: admin, user, guest" :path (:role) :value "superadmin"})

Spec shapes

A spec map pairs each key with one of:

  • a bare predicate{:name string?} (treated as required),
  • a validator built by the validate/* constructors,
  • a nested spec map{:address {:street string? :zip string?}} — validated recursively; error :paths accumulate, e.g. (:address :zip).

validate/required and validate/optional accept all three, so an entire nested map can be made optional:

(define sch (validate/schema {:address (validate/optional {:street string?})}))
(validate/valid? sch {})                          ; => #t (absent is fine)
(validate/valid? sch {:address {:street "Main"}}) ; => #t
(validate/valid? sch {:address {:street 42}})     ; => #f

API

Function Description
(validate/schema spec) Create a schema from a spec map
(validate/valid? schema data) #t if data matches, #f otherwise
(validate/errors schema data) List of error maps; empty list if valid
(validate/assert! schema data) Return data, or raise with readable details
(validate/required pred) Field is required (predicate, validator, or nested spec)
(validate/optional pred) Field may be absent; checked when present
(validate/one-of . values) Value must equal one of the values
(validate/all-of . preds) Value must satisfy every predicate/validator
(validate/list-of pred) List whose every element matches
(validate/matches pattern) String matching a regex
(validate/in-range lo hi) Number in the inclusive range
(validate/min-length n) String/list with at least n chars/elements
(validate/max-length n) String/list with at most n chars/elements

validate/schema

(validate/schema {:name string? :age number?})

Wraps a spec map. All other functions take the schema as their first argument.

validate/valid?

(validate/valid? user-schema {:name "Alice" :age 30
                              :email "a@b.co" :role "user"})
; => #t

validate/errors

Returns a list of {:path :message :value} maps — :path is the list of keys from the root to the failing field:

(validate/errors (validate/schema {:address {:zip (validate/matches #"^\d{4}$")}})
                 {:address {:zip "x"}})
; => ({:message "expected string matching ^\d{4}$" :path (:address :zip) :value "x"})

validate/assert!

Returns the data unchanged when valid; otherwise raises an error joining every failure, with nested paths dot-separated:

(validate/assert! user-schema {:address {}})
; error: validate/assert!: validation failed — age: required field missing;
;        email: required field missing; name: required field missing;
;        role: required field missing

validate/required / validate/optional

(validate/required string?)                    ; required predicate
(validate/optional (validate/in-range 0 5))    ; optional validator
(validate/required {:street string?})          ; required nested map

Bare predicates and validator constructors default to required; validate/optional is how a field becomes skippable.

validate/one-of

(validate/one-of "admin" "user" "guest")

Compares with equal?, so any value type works.

validate/all-of

(validate/all-of number? (validate/in-range 0 150))

Accepts a mix of bare predicates and validators.

validate/list-of

(validate/list-of string?)
(validate/list-of (validate/in-range 0 100))

Fails for non-lists; the empty list is valid.

validate/matches

(validate/matches #"^\d{4}$")

Fails for non-strings.

validate/in-range

(validate/in-range 0 150)   ; inclusive on both ends; fails for non-numbers

validate/min-length / validate/max-length

(validate/min-length 8)   ; strings: character count; lists: element count
(validate/max-length 64)

Fail for values that are neither strings nor lists.

Testing

sema pkg add sema-test   # once
sema tests.sema

License

MIT

VersionSizePublished
0.1.0 5 KB 2026-07-07 21:28:20
No dependencies