An excerpt from the repository, fetched Nov 13, 2026.
Read the project’s introduction
A Clojure(Script) library for declarative data description and validation.
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
Read the full README on GitHub