plumatic/schema

repository·master·Indexed 25 days ago

https://github.com/plumatic/schema

A Clojure(Script) library for declarative data description and validation. It provides a language for describing data shapes, enabling data validation, type annotation for functions and records using the `:-` syntax, and data coercion. It supports Clojure 1.8+, Babashka 0.8.156+, and the latest ClojureScript.

Tokens
2.5K
Snippets
10
Records
15
Agent score
32%

What's inside plumatic-schema

  1. Annotate functions and records with Schema

    master

    Schema extends Clojure's type hinting system using the :- syntax. This allows you to specify schemas for function arguments, return values, and record fields. This provides precise documentation that can be inspected at runtime.

    Key macros:

    • s/defprotocol
    • s/defrecord
    • s/defn
    • s/def
    • s/fn

    Example syntax: (defn my-func :- s/Str [arg :- s/Int]) means my-func takes an integer and returns a string.

    To inspect the schema of a record or function, use s/explain. To enable runtime validation for all functions annotated with schemas, use s/with-fn-validation.

    (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))
    
    ;; Inspecting schemas
    (s/explain StampedNames)
    
    ;; Enabling validation
    (s/with-fn-validation
      (stamped-names ["bob"]))
  2. Perform data coercion with schema-driven transformations

    master

    Coercion allows you to transform input data (e.g., converting JSON strings to Keywords or Numbers to specific types) before validation occurs. You can use built-in matchers like coerce/json-coercion-matcher or write custom transformations.

    (s/defschema CommentRequest
      {(s/optional-key :parent-comment-id) long
       :text String
       :share-services [(s/enum :twitter :facebook :google)]})
    
    (def parse-comment-request
      (coerce/coercer CommentRequest coerce/json-coercion-matcher))
    
    (parse-comment-request
        {:parent-comment-id (int 2128123123)
         :text "This is awesome!"
         :share-services ["twitter" "facebook"]})
    ;; => {:parent-comment-id 2128123123, :text "This is awesome!", :share-services [:twitter :facebook]}
  3. Enable Schema validation during development and testing

    master

    There are several ways to activate Schema validation depending on your workflow:

    1. In Tests: To globally enable schema validation within a test namespace, use the schema.test/validate-schemas fixture:

      (use-fixtures :once schema.test/validate-schemas)
    2. In REPL/Development: To turn on runtime validation for all annotated functions globally, use:

      (s/set-fn-validation! true)
    3. Always Validate Specific Functions: To force validation on a specific function regardless of global settings, use the ^:always-validate metadata:

      (s/defn ^:always-validate my-function [args] ...)
  4. Define and validate data with Schema

    master

    A Schema is a Clojure(Script) data structure that describes a data shape. You can use s/defschema to define reusable schemas and s/validate to check if a piece of data conforms to that schema. If validation fails, s/validate throws a RuntimeException with a descriptive error message showing exactly where the data deviated from the schema.

    Common cross-platform leaf schemas include:

    • s/Any
    • s/Bool
    • s/Num
    • s/Keyword
    • s/Symbol
    • s/Int
    • s/Str

    On the JVM, you can use classes for instance checks. In ClojureScript, you can use prototype functions.

    (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]}]})
  5. Define Set schemas

    master

    A homogeneous set is specified using a singleton set containing the desired schema, e.g., #{s/Str}. You can use s/conditional to add extra constraints, such as ensuring a set is non-empty.

    (s/defn NonEmptySet [s]
      (s/conditional
        (every-pred set? seq) #{s}))
    
    (s/validate (NonEmptySet s/Str) #{"a"})
    ;; => Ok
  6. Define Map schemas with required and optional keys

    master

    You can define map schemas that enforce specific key requirements. Use s/required-key to ensure a key is present, or s/optional-key to allow it to be absent. For keyword keys, you can omit the required-key wrapper. You can also combine specific keys with generic schemas (e.g., s/Str s/Str) to define rules for all other keys in the map.

    (s/defschema FooBar {(s/required-key :foo) s/Str (s/required-key :bar) s/Keyword})
    
    (s/validate FooBar {:foo "f" :bar :b})
    ;; => {:foo "f" :bar :b}
    
    (s/defschema FancyMap
      {(s/optional-key :foo) s/Keyword
       s/Str s/Str})
  7. Define Sequence schemas with positional requirements

    master

    Sequence schemas are implicitly nilable (validating against nil returns nil). You can use regex-like schemas to define specific values at certain positions:

    • s/one: A named entry (singleton).
    • s/optional: An optional entry.
    • A trailing schema: Describes the rest of the sequence (like a * operator).
    (s/defschema FancySeq
      [(s/one s/Str "s")
       (s/optional s/Keyword "k")
       s/Num])
    
    (s/validate FancySeq ["test" :k 1 2 3])
    ;; => all ok
  8. Use common schema utilities like maybe, eq, enum, and pred

    master

    The schema.core module provides several utility functions for building complex schemas:

    • s/Any: Matches any value.
    • s/maybe: Makes a schema nilable.
    • s/eq: Matches a specific value.
    • s/enum: Matches one of several specific values.
    • s/pred: Matches a predicate function.
    (s/validate (s/maybe s/Keyword) nil)
    (s/validate (s/eq :a) :a)
    (s/validate (s/enum :a :b :c) :a)
    (s/validate (s/pred odd?) 1)
  9. Use s/check and s/checker for non-exception validation

    master
    While s/validate throws an exception on failure, you can use s/check or s/checker when you want to handle errors programmatically. These functions return the error (or nil if the data is valid) instead of throwing an exception.
  10. Configure Schema error output verbosity

    master

    Schema limits the size of values in error messages to 19 characters to keep them readable. If a value is longer, it is replaced by its class name. You can adjust this limit using:

    set-max-value-length!

  11. Define recursive schemas

    master

    Recursive schemas can be defined by referencing the schema name itself within the definition, typically using s/recursive.

    (s/defschema Tree {:value s/Int :children [(s/recursive #'Tree)]})
    
    (s/validate Tree {:value 0, :children [{:value 1, :children []}]})
  12. Apply constraints with s/constrained

    master

    Use s/constrained to apply a predicate as a postcondition to a type. This is often preferred over s/conditional for adding extra validation to a single type because it typically provides better error messages and works better with coercion.

    (s/defschema OddLong (s/constrained long odd?))
    
    (s/validate OddLong 1)
    ;; => 1
    
    (s/validate OddLong 2)
    ;; => RuntimeException: Value does not match schema: (not (odd? 2))