SkillAgentSearch skills...

Schema

Clojure(Script) library for declarative data description and validation

Install / Use

/learn @plumatic/Schema
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<img src="https://raw.github.com/wiki/plumatic/schema/images/logo.png" width="270" />

A Clojure(Script) library for declarative data description and validation.

Clojars Project

API docs.

--

One of the difficulties with bringing Clojure into a team is the overhead of understanding the kind of data (e.g., list of strings, nested map from long to string to double) that a function expects and returns. While a full-blown type system is one solution to this problem, we present a lighter weight solution: schemas. (For more details on why we built Schema, check out this post.)

Schema is a rich language for describing data shapes, with a variety of features:

  • Data validation, with descriptive error messages of failures (targeted at programmers)
  • Annotation of function arguments and return values, with optional runtime validation
  • Schema-driven data coercion, which can automatically, succinctly, and safely convert complex data types (see the Coercion section below)
  • Other
    • Schema is also built into our plumbing and fnhouse libraries, which illustrate how we build services and APIs easily and safely with Schema
    • Schema also supports experimental clojure.test.check data generation from Schemas, as well as completion of partial datums, features we've found very useful when writing tests as part of the schema-generators library

Meet Schema

A Schema is a Clojure(Script) data structure describing a data shape, which can be used to document and validate functions and data.

(ns schema-examples
  (:require [schema.core :as s
             :include-macros true ;; cljs only
             ]))

(s/defschema Data
  "A schema for a nested data type"
  {:a {:b s/Str
       :c s/Int}
   :d [{:e s/Keyword
        :f [s/Num]}]})

(s/validate
  Data
  {:a {:b "abc"
       :c 123}
   :d [{:e :bc
        :f [12.2 13 100]}
       {:e :bc
        :f [-1]}]})
;; Success!

(s/validate
  Data
  {:a {:b 123
       :c "ABC"}})
;; Exception -- Value does not match schema:
;;  {:a {:b (not (instance? java.lang.String 123)),
;;       :c (not (integer? "ABC"))},
;;   :d missing-required-key}

The simplest schemas describe leaf values like Keywords, Numbers, and instances of Classes (on the JVM) and prototypes (in ClojureScript):

;; s/Any, s/Bool, s/Num, s/Keyword, s/Symbol, s/Int, and s/Str are cross-platform schemas.

(s/validate s/Num 42)
;; 42
(s/validate s/Num "42")
;; RuntimeException: Value does not match schema: (not (instance java.lang.Number "42"))

(s/validate s/Keyword :whoa)
;; :whoa
(s/validate s/Keyword 123)
;; RuntimeException: Value does not match schema: (not (keyword? 123))

;; On the JVM, you can use classes for instance? checks
(s/validate java.lang.String "schema")

;; On JS, you can use prototype functions
(s/validate Element (js/document.getElementById "some-div-id"))

From these simple building blocks, we can build up more complex schemas that look like the data they describe. Taking the examples above:

;; list of strings
(s/validate [s/Str] ["a" "b" "c"])

;; nested map from long to String to double
(s/validate {long {String double}} {1 {"2" 3.0 "4" 5.0}})

Since schemas are just data, you can also def them and reuse and compose them as you would expect:

(def StringList [s/Str])
(def StringScores {String double})
(def StringScoreMap {long StringScores})

However, we encourage you to use s/defschema for this purpose to improve error messages:

(s/defschema StringList [s/Str])
(s/defschema StringScores {String double})
(s/defschema StringScoreMap {long StringScores})

What about when things go bad? Schema's s/check and s/validate provide meaningful errors that look like the bad parts of your data, and are (hopefully) easy to understand.

(s/validate StringList ["a" :b "c"])
;; RuntimeException: Value does not match schema:
;;  [nil (not (instance? java.lang.String :b)) nil]

(s/validate StringScoreMap {1 {"2" 3.0 "3" [5.0]} 4.0 {}})
;; RuntimeException: Value does not match schema:
;;  {1 {"3" (not (instance? java.lang.Double [5.0]))},
;;   (not (instance? java.lang.Long 4.0)) invalid-key}

See the More examples section below for more examples and explanation, or the custom Schemas types page for details on how Schema works under the hood.

Beyond type hints

If you've done much Clojure, you've probably seen code with documentation like this:

(defprotocol TimestampOffsetter
  (offset-timestamp [this offset] "adds integer offset to stamped object and returns the result"))

(defrecord StampedNames
  [^Long date
   names] ;; a list of Strings
  TimestampOffsetter
  (offset [this offset] (+ date offset)))

(defn ^StampedNames stamped-names
  "names is a list of Strings"
  [names]
  (StampedNames. (str (System/currentTimeMillis)) names))

(def ^StampedNames example-stamped-names
  (stamped-names (map (fn [first-name] ;; takes and returns a string
                        (str first-name " Smith"))
                      ["Bob" "Jane"])))

Clojure's type hints make great documentation, but they fall short for complex types, often leading to ad-hoc descriptions of data in comments and doc-strings. This is better than nothing, but these ad hoc descriptions are often imprecise, hard to read, and prone to bit-rot.

Schema provides macros s/defprotocol, s/defrecord, s/defn, s/def, and s/fn that help bridge this gap. These macros are just like their clojure.core counterparts, except they support arbitrary schemas as type hints on fields, arguments, and return values. This is a graceful extension of Clojure's type hinting system, because every type hint is a valid Schema, and Schemas that represent valid type hints are automatically passed through to Clojure.

(s/defprotocol TimestampOffsetter
  (offset-timestamp :- s/Int [this offset :- s/Int]))

(s/defrecord StampedNames
  [date :- Long
   names :- [s/Str]]
  TimestampOffsetter
  (offset [this offset] (+ date offset)))

(s/defn stamped-names :- StampedNames
  [names :- [s/Str]]
  (StampedNames. (str (System/currentTimeMillis)) names))

(s/def example-stamped-names :- StampedNames
  (stamped-names (map (s/fn :- s/Str [first-name :- s/Str]
                        (str first-name " Smith"))
                      ["Bob" "Jane"])))

Here, x :- y means that x must satisfy schema y, replacing and extending the more familiar metadata hints such as ^y x.

As you can see, these type hints are precise, easy to read, and shorter than the comments they replace. Moreover, they produce Schemas that are data, and can be inspected, manipulated, and used for validation on-demand (did you spot the bug in stamped-names?)

;; You can inspect the schemas of the record and function

(s/explain StampedNames)
==> (record user.StampedNames {:date java.lang.Long, :names [java.lang.String]})

(s/explain (s/fn-schema stamped-names))
==> (=> (record user.StampedNames {:date java.lang.Long, :names [java.lang.String]})
        [java.lang.String])

;; And you can turn on validation to catch bugs in your functions and schemas
(s/with-fn-validation
  (stamped-names ["bob"]))
==> RuntimeException: Output of stamped-names does not match schema:
     {:date (not (instance? java.lang.Long "1378267311501"))}

;; Oops, I guess we should remove that `str` from `stamped-names`.

Schemas in practice

We've already seen how we can build up Schemas via composition, attach them to functions, and use them to validate data. What does this look like in practice?

First, we ensure that all data types that will be shared across namespaces (or heavily used within namespaces) have Schemas, either by defing them or using s/defrecord. This allows us to compactly and precisely refer to this data type in more complex data types, or when documenting function arguments and return values.

This documentation is probably the most important benefit of Schema, which is why we've optimized Schemas for easy readability and reuse -- and sometimes, this is all you need. Schemas are purely descriptive, not prescriptive, so unlike a type system they should never get in your way, or constrain the types of functions you can write.

After documentation, the next-most important benefit is validation. Thus far, we've found four key use cases for validation. First, you can globally turn on function validation within a given test namespace by adding this line:

(use-fixtures :once schema.test/validate-schemas)

As long as your tests cover all call boundaries, this means you should catch any 'type-like' bugs in your code at test time.

Second, it may be handy to enable schema validation during development. To enable it, you can either type this into the repl or put it in your user.clj:

(s/set-fn-validation! true)

To disable it again, call the same function, but with false as parameter instead.

Third, we manually call s/validate to check any data we read and write over the wire or to persistent storage, ensuring that we catch and debug bad data before it strays too far from its source. If you need maximal performance, you can avoid the schema processing overhead on each call by create a validator once with s/validator and calling the resulting function on each datum you want to validate (s/defn does this under the hood). Analogously, s/check and s/checker are similar, but return the error (or nil for success) rather than throwing exceptions on

Related Skills

View on GitHub
GitHub Stars2.5k
CategoryDevelopment
Updated10d ago
Forks251

Languages

Clojure

Security Score

80/100

Audited on Mar 13, 2026

No findings