validate:   Schema Validation and Typed JSON Decoding
1 Parsing
schema?
parse
parse-safe
ok
err
validate-error
render-error
exn:  fail:  validate?
exn:  fail:  validate-errors
2 Schema combinators
string-schema
int-schema
real-schema
symbol-schema
bytes-schema
boolean-schema
null-schema
any-schema
literal
enum
list-of
vector-of
tuple
hash-of
field
field
object
union
maybe
refine
map-schema
lazy
3 The JSON boundary
jsexpr-schema
to-jsexpr
jsexpr-encode
json-value?
9.3

validate: Schema Validation and Typed JSON Decoding🔗ℹ

 (require validate) package: validate

Schema validation for untrusted data — the zod workflow in Racket: declare a schema once, parse with it everywhere, get verified values or path-addressed errors. The killer application is the JSON boundary: jsexpr-schema gates decoded JSON before a schema looks at it, and to-jsexpr encodes everyday Racket data (symbols, keywords, vectors, mutable hashes) without dying in a json-module contract.

1 Parsing🔗ℹ

procedure

(schema? v) → boolean?

  v : any/c

Recognizes schemas built by this library.

procedure

(parse s v) → any/c

  s : schema?
  v : any/c

Validates v, returning the verified (possibly transformed) value. On failure raises exn:fail:validate? carrying every error.

procedure

(parse-safe s v) → (or/c ok? err?)

  s : schema?
  v : any/c

The non-raising form: (ok value) or (err errors).

struct

(struct ok (value))

  value : any/c

struct

(struct err (errors))

  errors : (listof validate-error?)

struct

(struct validate-error (path expected got message))

  path : (listof (or/c symbol? exact-integer? string?))
  expected : string?
  got : any/c
  message : string?

path runs from the root: keys, then bracketed indices.

procedure

(render-error e) → string?

  e : validate-error?
formats one, e.g. "items.[1].title: expected a string (got: 99)".

procedure

(exn:fail:validate? v) → boolean?

  v : any/c

procedure

(exn:fail:validate-errors e) → (listof validate-error?)

  e : exn:fail:validate?

2 Schema combinators🔗ℹ

Every constructor is a function returning a schema?; pass one around, compose freely, recurse via lazy.

procedure

(string-schema [#:min-length min-length    
  #:max-length max-length    
  #:pattern pattern]) → schema?
  min-length : (or/c exact-nonnegative-integer? #f) = 0
  max-length : (or/c exact-nonnegative-integer? #f) = #f
  pattern : (or/c regexp? #f) = #f

procedure

(int-schema [#:min min #:max max]) → schema?

  min : (or/c exact-integer? #f) = #f
  max : (or/c exact-integer? #f) = #f

Exact integers only — 3.0 is rejected.

procedure

(real-schema [#:min min #:max max]) → schema?

  min : (or/c real? #f) = #f
  max : (or/c real? #f) = #f

procedure

(symbol-schema) → schema?

procedure

(bytes-schema) → schema?

procedure

(boolean-schema) → schema?

procedure

(null-schema) → schema?

procedure

(any-schema) → schema?

procedure

(literal v) → schema?

  v : any/c

procedure

(enum choices) → schema?

  choices : (listof any/c)

procedure

(list-of s    
  [#:min-length min-length    
  #:max-length max-length]) → schema?
  s : schema?
  min-length : exact-nonnegative-integer? = 0
  max-length : (or/c exact-nonnegative-integer? #f) = #f

Accepts lists, reports element errors as [index]-addressed paths.

procedure

(vector-of s) → schema?

  s : schema?

procedure

(tuple schemas) → schema?

  schemas : (listof schema?)

procedure

(hash-of key value) → schema?

  key : schema?
  value : schema?

Open hashes; keys and values each validated.

struct

(struct field (key schema optional? default))

  key : symbol?
  schema : schema?
  optional? : boolean?
  default : any/c

procedure

(field key    
  s    
  [#:optional optional?    
  #:default default]) → field?
  key : (or/c symbol? string?)
  s : schema?
  optional? : boolean? = #f
  default : any/c = #f

procedure

(object #:fields fields [#:closed? closed?]) → schema?

  fields : (listof field?)
  closed? : boolean? = #f

The workhorse. Lookups accept string or symbol keys, from equal- or eq-based hashes alike — JSON decoders and hand-built hasheqs both decode. Output is an immutable hash with symbol keys, containing every declared field (optional ones filled with their default), so downstream code can hash-ref without guards. Unknown keys are stripped; with #:closed? #t they become errors. Missing required fields name the field in the error.

procedure

(union schemas) → schema?

  schemas : (listof schema?)

First alternative without errors wins.

procedure

(maybe s [#:default default]) → schema?

  s : schema?
  default : any/c = #f

Maps #f and JSON 'null to default; everything else goes through s.

procedure

(refine s pred message) → schema?

  s : schema?
  pred : (-> any/c boolean?)
  message : string?

procedure

(map-schema s f) → schema?

  s : schema?
  f : (-> any/c any/c)

procedure

(lazy thunk) → schema?

  thunk : (-> schema?)

Recursive schemas:

(define tree
  (object #:fields
          (list (field 'value (int-schema))
                (field 'children (lazy (λ () (list-of tree)))
                       #:optional #t #:default '()))))

3 The JSON boundary🔗ℹ

procedure

(jsexpr-schema s) → schema?

  s : schema?

Gates on JSON representability first, so input a schema would happily accept (a symbol, a dotted pair) still gets rejected with a JSON-shaped error.

procedure

(to-jsexpr v) → json-value?

  v : any/c

Normalizes symbols, keywords, characters, vectors, proper pairs, and mutable hashes into JSON-representable shapes; 'null stays JSON null; anything else (structs, dotted pairs) raises exn:fail:validate? with a message naming the culprit. Hash keys normalize to symbols (what this Racket’s write-json requires).

procedure

(jsexpr-encode v) → string?

  v : any/c

(jsexpr->string (to-jsexpr v)).

procedure

(json-value? v) → boolean?

  v : any/c

JSON representability, accepting string and symbol hash keys — different Racket builds’ read-json disagree.