JSON Schema 2020-12 validation for Clojure. No dependencies, no reflection,
native-image safe. skjema is Norwegian for schema.
com.blockether/skjema {:mvn/version "0.1.0"}
Every JVM validator worth using binds Jackson databind, which means reflection
and a reachability-metadata chase in a GraalVM native image, plus a JSON stack a
consumer did not choose. skjema binds nothing:
.json, copied straight from the
specification or the test suite, readable by every other tool that speaks
JSON Schema. No EDN dialect, no translation layer, no drift test between the
two spellings.compile indexes a schema once;
validate walks the instance, not the schema map. The instance may come
from skjema.json, from charred, from anywhere — it is plain Clojure data.valid,
instanceLocation, keywordLocation, absoluteKeywordLocation, error —
as a Clojure map, so writing it back out is one call and no second
vocabulary exists.(require '[com.blockether.skjema.core :as skjema]
'[com.blockether.skjema.json :as json])
(def schema (json/read-str (slurp "user.schema.json")))
(skjema/valid? schema {"name" "Ada"})
;; => true
(skjema/validate schema {"name" 42})
;; => {:valid false
;; :errors [{:instanceLocation "/name"
;; :keywordLocation "/properties/name/type"
;; :absoluteKeywordLocation "https://example.com/user.json#/properties/name/type"
;; :error "expected string, got integer"}]}
(json/write-str (skjema/validate schema {"name" 42}))
;; => the same answer as the specification's BASIC output, key for key
compile once when the schema is reused, and hand it whatever it references —
nothing is ever fetched:
(def compiled (skjema/compile schema {:base "https://example.com/user.json"
:registry {"https://example.com/tag.json" tag-schema}}))
(skjema/valid? compiled instance)
A document that references its neighbours by relative path works the same way:
the registry decides what a name means, so the schemas can live in
resources/ and be read with io/resource — this library reads nothing off
the filesystem and opens no socket. A reference nobody supplied is a compile
error, never a silent pass:
(def registry
(into {} (for [n ["node.json" "action.json"]]
[n (json/read-str (slurp (io/resource (str "contract/" n))))])))
(skjema/compile view-schema {:base "view.json" :registry registry})
;; "$ref": "node.json" inside it resolves against the base, to the registry key
The 2020-12 meta-schemas ARE read from this jar's own resources, and the jar
carries the META-INF/native-image metadata that registers them — a consumer
builds a GraalVM native image without knowing that.
format annotates rather than asserts, which is what the specification says by
default. Ask for the assertion — every format 2020-12 names is implemented,
date-time through idn-hostname — with an option, or by declaring the
format-assertion vocabulary in a meta-schema:
(skjema/valid? {"format" "idn-hostname"} "\u5b9f\u4f8b.\u30c6\u30b9\u30c8" {:format-assertion true})
Green, and the gate says what that means:
optional/ files — the formats, the
ECMAScript regular expressions, arbitrary-precision numbers, the draft-07
dependencies, and a reference that crosses into an older draft.y_ accepted, n_ refused, i_ answered).Two things the suite does not ask for and this library does not do: it never
fetches a document over the network, and it evaluates the 2020-12 dialect —
a resource that declares 2019-09, draft-07, draft-06 or draft-04 is read with
the keywords THAT draft defines, so $recursiveRef is recognized without being
followed.
clojure -M:bench measures the same data against
malli, both sides prepared once, with
criterium. One machine's medians — run it on yours:
| what | skjema | malli |
|---|---|---|
| object of 9 members, valid | 2.8 us | 0.32 us |
| the same object, two members wrong | 0.75 us | 0.11 us |
| the same object, errors reported | 5.0 us | 0.58 us |
one string, minLength/maxLength | 110 ns | 13 ns |
| array of 1000 integers with bounds | 100 us | 8.3 us |
| preparing the schema itself | 35 us | 5.3 us |
malli compiles a schema written in its own language down to closures; skjema
walks a JSON document the specification defines keyword by keyword, and stays
within an order of magnitude of it. What it costs is decided when the schema is
compiled, never per instance:
valid? is fail-fast and location-free — it stops at the first refusal
and never builds the JSON Pointers only an error would print;unevaluatedProperties
or unevaluatedItems, because nothing else can read them;$ref resolution is cached per compiled schema.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |