Git Product home page Git Product logo

schema's Introduction

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 bad data.

Alternatively, you can force validation for key functions (without the need for with-fn-validation):

(s/defn ^:always-validate stamped-names ...)

Thus, each time you invoke stamped-names, Schema will perform validation.

To reduce generated code size, you can use the *assert* flag and set-compile-fn-validation! functions to control when validation code is generated (details).

Schema will attempt to reduce the verbosity of its output by restricting the size of values that fail validation to 19 characters. If a value exceeds this, it will be replaced by the name of its class. You can adjust this size limitation by calling set-max-value-length!.

Finally, we use validation with coercion for API inputs and outputs. See the coercion section below for details.

More examples

The source code in schema/core.cljc provides a wealth of extra tools for defining schemas, which are described in docstrings. The file schema/core_test.cljc demonstrates a variety of sample schemas and many examples of passing & failing clojure data. We'll just touch on a few more examples here, and refer the reader to the code for more details and examples (for now).

Map schemas

In addition to uniform maps (like String to Double), map schemas can also capture maps with specific key requirements:

(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/validate FooBar {:foo :f})
;; RuntimeException: Value does not match schema:
;;  {:foo (not (instance? java.lang.String :f)),
;;   :bar missing-required-key}

For the special case of keywords, you can omit the required-key, like {:foo s/Str :bar s/Keyword}. You can also provide specific optional keys, and combine specific keys with generic schemas for the remaining key-value mappings:

(s/defschema FancyMap
  "If foo is present, it must map to a Keyword.  Any number of additional
   String-String mappings are allowed as well."
  {(s/optional-key :foo) s/Keyword
    s/Str s/Str})

(s/validate FancyMap {"a" "b"})

(s/validate FancyMap {:foo :f "c" "d" "e" "f"})

Sequence schemas

Unlike most schemas, sequence schemas are implicitly nilable:

(s/validate [s/Any] nil)
;=> nil

You can also write sequence schemas that expect particular values in specific positions using some regex-like schemas. s/one is a named entry (like a singleton cat in clojure.spec), s/optional is an optional entry (like ? in regular expressions), and a trailing schema describes the rest of the sequence (like * in regular expressions).

(s/defschema FancySeq
  "A sequence that starts with a String, followed by an optional Keyword,
   followed by any number of Numbers."
  [(s/one s/Str "s")
   (s/optional s/Keyword "k")
   s/Num])

(s/validate FancySeq ["test"])
(s/validate FancySeq ["test" :k])
(s/validate FancySeq ["test" :k 1 2 3])
;; all ok

(s/validate FancySeq [1 :k 2 3 "4"])
;; RuntimeException: Value does not match schema:
;;  [(named (not (instance? java.lang.String 1)) "s")
;;   nil nil nil
;;   (not (instance? java.lang.Number "4"))]

Set schemas

A homogeneous set of values is specified by a singleton set. A set of strings is #{s/Str}.

Use s/conditional to add additional constraints:

(s/defn NonEmptySet [s]
  (s/conditional
    (every-pred set? seq) #{s}))

(s/validate (NonEmptySet s/Str) #{})
;; Fail
(s/validate (NonEmptySet s/Str) #{"a"})
;; Ok

Other schema types

schema.core provides many more utilities for building schemas, including s/maybe, s/eq, s/enum, s/pred, s/conditional, s/cond-pre, s/constrained, and more. Here are a few of our favorites:

;; anything
(s/validate s/Any "woohoo!")
(s/validate s/Any 'go-nuts)
(s/validate s/Any 42.0)
(s/validate [s/Any] ["woohoo!" 'go-nuts 42.0])

;; maybe (nilable)
(s/validate (s/maybe s/Keyword) :a)
(s/validate (s/maybe s/Keyword) nil)

;; eq and enum
(s/validate (s/eq :a) :a)
(s/validate (s/enum :a :b :c) :a)

;; pred
(s/validate (s/pred odd?) 1)

;; conditional (i.e. variant or option)
(s/defschema StringListOrKeywordMap (s/conditional map? {s/Keyword s/Keyword} :else [String]))
(s/validate StringListOrKeywordMap ["A" "B" "C"])
;; => ["A" "B" "C"]
(s/validate StringListOrKeywordMap {:foo :bar})
;; => {:foo :bar}
(s/validate StringListOrKeywordMap [:foo])
;; RuntimeException:  Value does not match schema: [(not (instance? java.lang.String :foo))]

;; if (shorthand for conditional)
(s/defschema StringListOrKeywordMap (s/if map? {s/Keyword s/Keyword} [String]))

;; cond-pre (experimental), also shorthand for conditional, allows you to skip the
;; predicate when the options are superficially different by doing a greedy match
;; on the preconditions of the options.
(s/defschema StringListOrKeywordMap (s/cond-pre {s/Keyword s/Keyword} [String]))
;; but don't do this -- this will never validate `{:b :x}` because the first schema
;; will be chosen based on the `map?` precondition (use `if` or `abstract-map-schema` instead):
(s/defschema BadSchema (s/cond-pre {:a s/Keyword} {:b s/Keyword}))

;; conditional can also be used to apply extra validation to a single type,
;; but constrained is often more desirable since it applies the validation
;; as a *postcondition*, which 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))
(s/validate OddLong (int 3))
;; RuntimeException: Value does not match schema: (not (instance? java.lang.Long 3))

;; recursive
(s/defschema Tree {:value s/Int :children [(s/recursive #'Tree)]})
(s/validate Tree {:value 0, :children [{:value 1, :children []}]})

;; abstract-map (experimental) models "abstract classes" and "subclasses" with maps.
(require '[schema.experimental.abstract-map :as abstract-map])
(s/defschema Animal
  (abstract-map/abstract-map-schema
   :type
   {:name s/Str}))
(abstract-map/extend-schema Cat Animal [:cat] {:claws? s/Bool})
(abstract-map/extend-schema Dog Animal [:dog] {:barks? s/Bool})
(s/validate Cat {:type :cat :name "melvin" :claws? true})
(s/validate Animal {:type :cat :name "melvin" :claws? true})
(s/validate Animal {:type :dog :name "roofer" :barks? true})
(s/validate Animal {:type :cat :name "confused kitty" :barks? true})
;; RuntimeException: 
;; Value does not match schema: {:claws? missing-required-key, :barks? disallowed-key}

You can also define schemas for recursive data types, or create your own custom schemas types.

Transformations and Coercion

Schema also supports schema-driven data transformations, with coercion being the main application fleshed out thus far. Coercion is like validation, except a schema-dependent transformation can be applied to the input data before validation.

An example application of coercion is converting parsed JSON (e.g., from an HTTP post request) to a domain object with a richer set of types (e.g., Keywords).

(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]})
;; ==> true

Here, json-coercion-matcher provides some useful defaults for coercing from JSON, such as:

  • Numbers should be coerced to the expected type, if this can be done without losing precision.
  • When a Keyword is expected, a String can be coerced to the correct type by calling keyword

There's nothing special about json-coercion-matcher though; it's just as easy to make your own schema-specific transformations to do even more.

For more details, see this blog post.

For the Future

Longer-term, we have lots more in store for Schema. Just a couple of the crazy ideas we have brewing are:

  • Automatically generate API client libraries based on API schemas
  • Compile to core.typed annotations for more typey goodness, if that's your thing

Community

Please feel free to join the Plumbing mailing list to ask questions or discuss how you're using Schema.

We welcome contributions in the form of bug reports and pull requests; please see CONTRIBUTING.md in the repo root for guidelines. Libraries that extend schema with new functionality are great too; here are a few that we know of:

If you make something new, please feel free to PR to add it here!

Supported Clojure versions

Schema is currently supported on Clojure 1.8 onwards, Babashka 0.8.156 onwards, and the latest version of ClojureScript.

License

Distributed under the Eclipse Public License, the same as Clojure.

schema's People

Contributors

abeppu avatar alexanderkiel avatar aria42 avatar bmabey avatar cpetzold avatar danielneal avatar davegolland avatar dijonkitchen avatar fmjrey avatar frenchy64 avatar gfredericks avatar ikitommi avatar jstepien avatar kachayev avatar kgann avatar krisajenkins avatar loganlinn avatar marcopolo avatar michaelblume avatar mpenet avatar nahuel avatar noprompt avatar oliyh avatar plredmond avatar rkday avatar sritchie avatar thebusby avatar vpagliari avatar w01fe avatar williammizuta avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

schema's Issues

schema functions are deactivated in a specific context

I haven't been able to isolate the bug, so I'm just posting it here for reference.

When I use prismatic/schema 0.2.2 in my project schema.core/validate and other schema functions do not work anymore. I.e. this test fails:

(fact "Schema is working"
      (schema/validate String 1) => (throws Exception))

I have tried some variations to see where it could go wrong (blank project etc), but I haven't found the ingredients that disable schema other than in my current (big) project with schema 0.2.2. When I downgrade to 0.2.1 or 0.2.0 everything is fine.

Not Nil Schema

How would I define a schema that is anything but nil?

Clojure warnings when using schema in a project with aot

I'm getting the following clojure warning by using schema.core:

WARNING: String already refers to: class java.lang.String in namespace: schema.core, being replaced by: #'schema.core/String
WARNING: Number already refers to: class java.lang.Number in namespace: schema.core, being replaced by: #'schema.core/Number

Not sure what the problems are that it can cause, but they are suspicious. I'm also not sure how to fix it. I think it has to do with this line https://github.com/Prismatic/schema/blob/master/src/cljx/schema/core.cljx#L92

I have a project as specified here https://gist.github.com/jeroenvandijk/6636009 . Here is some console output I get:

➜  schema-test  lein run
Compiling schema-test.core
WARNING: String already refers to: class java.lang.String in namespace: schema.core, being replaced by: #'schema.core/String
WARNING: Number already refers to: class java.lang.Number in namespace: schema.core, being replaced by: #'schema.core/Number
hi
➜  schema-test  lein uberjar
Created /Users/jeroen/tmp/schema-test/target/schema-test-0.1.0-SNAPSHOT.jar
Created /Users/jeroen/tmp/schema-test/target/schema-test-0.1.0-SNAPSHOT-standalone.jar
➜  schema-test  java -jar target/schema-test-0.1.0-SNAPSHOT-standalone.jar
WARNING: String already refers to: class java.lang.String in namespace: schema.core, being replaced by: #'schema.core/String
WARNING: Number already refers to: class java.lang.Number in namespace: schema.core, being replaced by: #'schema.core/Number
hi

JSON coercion doesn't work for maps with explicit keywords

The following produces a disallowed key error.

(require ['schema.core :as 's])
(require ['schema.coerce :as 'c])
((c/coercer {:a s/Int} c/json-coercion-matcher) {"a" 1})

Since keys in maps are not always "walked", there is no simple way to provide this coercion without looking at the entire map schema passed into json-coercion-matcher and adjusting the keys there. It would seem to be cleaner to walk all map keys so you could rely on the string->keyword function already present in schema.coerce.

Coercion for Inst type?

Hey all,

I'm wondering if there's any plans for adding coercion support for Inst? I'm happy to put together a PR if that is useful.

nil validates as a vector/seq

=> (validate [Int] nil)
nil

I expect the schema [Int] to require that a value be a vector (or sequence, I guess). So I want [] or [1 2 3] to be valid, but not nil.

Recursive definitions won't work?

Hi,

is there a way to make recursive definitions work? I tried:

(ns schema-test (:require [schema.core :as s]))

(def Foo {(s/optional-key :foo) Foo})

(s/validate Foo {:foo {:foo {}}})

; -> IllegalArgumentException No implementation of method: :check of protocol: #'schema.core/Schema found for class: clojure.lang.Var$Unbound  clojure.core/-cache-protocol-fn (core_deftype.clj:541)

throws? non-standard

May want to add what library you're using for throws? in the readme or add it to schema itself.

java.lang.RuntimeException: Unable to resolve symbol: throws? in this context

strings can't be used as keys

It appears strings should validate in this case, similar to keywords:

(schema.core/validate
  {:a schema.core/Int
   :b schema.core/Int}
  {:a 1
   :b 2})

;;=> {:b 2, :a 1}


(schema.core/validate
  {"a" schema.core/Int
   "b" schema.core/Int}
  {"a" 1
   "b" 2})

;;=> RuntimeException More than one non-optional/required key schemata: ["a" "b"]  schema.core/find-extra-keys-schema (core.clj:654)

schema.coerce - support String -> s/Bool [0.2.1]

Neither json-coercion-matcher nor string-coercion-matcher will coerce a string value to a boolean.

This is desirable for me, as sometimes I'm apply schema to query parameters and want to convert "true" and "false" to true and false.

error message for Strings (not) in collections

Would it be possible to have a better error message for:

(s/check
  {:foo [s/String]}
  {:foo "abc"})

;=> {:foo [(not (instance? java.lang.String \a)) (not (instance? java.lang.String \b)) (not (instance? java.lang.String \c))]}

JSON coercion doesn't coerce sequences of `s/either` schemas as expected

When using JSON coercion with a custom coercer into an either schema, sequences of elements which should enter an either schema aren't parsed as I expect. Here is a reduced example:

(ns coercion-bug
  (:require [schema.core :as s]
            [schema.coerce :as sc]))

(s/defrecord S [s :- s/Str])
(s/defrecord I [i :- s/Int])
(def SI (s/either S I))

(s/defn parse-SI :- SI
  [v]
  (if (string? v) (->S v) (->I v)))
(def schema-coercions {SI parse-SI})

(def parse-sis
  (sc/coercer [SI]
              (fn [schema]
                (or (schema-coercions schema)
                    (sc/json-coercion-matcher schema)))))

(parse-sis ["a" 2 "c"])
;; ==> [#coercion_bug.S{:s "a"} #schema.utils.ErrorContainer{:error (not (instance? coercion_bug.S a-coercion_bug.I))} #coercion_bug.S{:s "c"}]

Here parse-SI consumes a value and decides whether to yield an S or an I based on the value's concrete type. I expected parse-sis to apply the coercion matcher parse-SI for each element of the sequence which is expected to be an SI. Instead it appears that the expected final type of the first parsed element is taken as the schema for all members of the sequence -- the fact that SI is an either schema is lost.

"clojure.lang.Var$Unbound" error on AOT'ed schemas

Okay, let me get a little more specific with my bug report :)

I wrote a clone-ns macro so that I could slowly start migrating my clj-only code over to cljx files in a different root without breaking all of my current imports. Each clj model calls clone-ns on its cljx counterpart.

clone-ns looks at all public vars in the target namespace and aliases them into the namespace where clone-ns is called. Here's the code:

https://github.com/sritchie/schema-repro/blob/master/src/clj/schema_error/util.clj#L21

This macro great in development mode at the repl, but fails when I try to AOT compile it and deploy it into production.

Here's a minimal repro of the issue: https://github.com/sritchie/schema-repro

If I use instead of clone-ns, the issue goes away. (I can't do this in my project, since I want to alias the vars and expose them to other namespaces.)

Any idea how to deal with this? Is there some metadata that's not getting transferred over? Honestly, I'm just sure WHICH damned var is unbound.

fn-schema and clojurescript

Hi,

I can't get schema.core/fn-schema to work when used in a clojurescript environnement.

For example:

 (defn my-fn :- s/Str [x :- s/Str] x)
 (fn-chema my-fn)

gives me: "Error: Key :schema not found in null"

It seems that "defn" registers the FnSchema using schema.utils/type-of. In clojurescript, schema.utils/type-of returns the string "function" and not the function object itself.

nested vectors not validating

This works:

(s/validate [(s/one s/Int :some-int) [s/Int]] [4 [3 2]])

The following does not:

(s/validate [[s/Int] (s/one s/Int :some-int)] [[4 3] 2])

I receive: "RuntimeException Sequence schema [[Int] #schema.core.One{:schema Int, :optional? false, :name :some-int}] does not match [one* optional* rest-schema?] schema.core/parse-sequence-schema (core.clj:746)"

s/defn-

It would be nice if there was a s/defn- so that I would not have to make a private function like this:

(s/defn something [....] ^:private .... 

`schema.coerce/coercer` should preserve uncoerced originals in errors.

We're using schema to validate Yaml; specifically, we annotate our Yaml data with source positions and then use coercions to validate the un-annotated data. We'd like to display nice error messages using the source positions, but schema.coerce/coercer doesn't preserve the original uncoerced data in the error it throws.

I've written an alternative coercer that does this, but I'm not convinced that it's the best way to do it. What do you all think?

;; A unique type we'll wrap ordinary schema errors in in order to provide
;; the original annotated-with-yaml-positions value.
(deftype CoercionError [original error])

(defn annotate-error
  "Turn an error in an ErrorContainer into a CoercionError containing it
   and the original uncoerced version of the value in question. Preserves
   the value of all other types."
  [original result]
  (if (u/error? result)
    (let [inner (:error result)]
      (u/error
       (cond
        ;; If we get an ordinary Validation error (e.g. 1 is not s/Str),
        ;; just wrap it in a CoercionError.
        (instance? schema.utils.ValidationError inner)
        (CoercionError. original inner)
        ;; If we get a map, it may contain e.g. [:x 'disallowed-key];
        ;; so wrap all symbols in a CoercionError.
        ;; N.B. -- original here is the original map, *not* the original key.
        (map? inner)
        (into {}
              (for [[k v] inner]
                [k (if (symbol? v)
                     (CoercionError. original v)
                     v)]))
        ;; Otherwise we don't care about wrapping it.
        :default inner)))
    result))

(defn coercer
  "An alternative to `schema.coerce/coercer` that preserves uncoerced originals."
  [schema coercion-matcher]
  (s/start-walker
   (fn [s]
     (let [walker (s/walker s)]
       (if-let [coercer (coercion-matcher s)]
         (fn [x]
           (annotate-error x
            (m/try-catchall
             (let [v (coercer x)]
               (if (u/error? v)
                 v
                 (walker v)))
             (catch t (m/validation-error s x t)))))
         walker)))
   schema))

(This is a little less-than-perfect, especially since for e.g. 'disallowed-key we get the original uncoerced map rather than the original key. In general coercing keys seems like a hard problem; I'd love advice on this).

Data generation with schema

In the readme you said that you want to provide the ability to generate data from a given schema.

Since I will need a feature like this in the near future I’d like to know if this is still a planned feature and, if so, whether or not you’d like to discuss its design here.

I’m willing to contribute some effort and spare time to implement this.

TIA

Advanced Compilation error in CLJS defrecords

Another one for your radar.

(ns mynamespace
  (:require [schema.core :as s])
  (#+clj :require #+cljs :require-macros [schema.macros :as sm]))

(sm/defrecord ResultEntry [id :- s/Int time :- s/Int])
(sm/defrecord StopwatchState
    [active? :- #+clj Boolean #+cljs js/Boolean
     stopwatch-time :- s/Int
     results :- [ResultEntry]
     timestamp :- s/Int])

Generates this error:

Uncaught TypeError: Object #<Aj> has no method 'Ld' generated.js:348
gk generated.js:348
(anonymous function) generated.js:349
(anonymous function)

Here are the relevant deps:

[org.clojure/clojurescript "0.0-2014"]
[lein-cljsbuild "1.0.0-alpha2"]
[org.clojure/tools.reader "0.7.10"]

Prior to this upgrade, on 193something, I was seeing the same sort of error, but with "no method 'java'". Perhaps a clue?

The error message given when a sequence schema gets invalid data can miss out the cause

If the output of a function doesn't match a sequence schema, like this:

(sm/defn test-error-msg :- [Number]
  []
  [1 2 3 4 5 6 7 8 9 10 "Not a number"])

(test-error-msg)

You get a runtime exception like this:

RuntimeException Output of test-schema does not match schema: 
[nil nil nil nil nil nil nil nil nil nil (not (instance? java.lang.Number "Not a number"))]  

This is ok if the sequence is short - but I often seem to have a long sequence which gets truncated before the crucial point:

RuntimeException Output of test-schema does not match schema: 
[nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil ...]

I think it would be great if the valid data could be skipped:

RuntimeException Output of test-schema does not match schema: 
[... (not (instance? java.lang.Number "Not a number"))]  

(or perhaps if the invalid data could be given on the :ex-info of the exception)

(:ex-info *e)
;=> {:invalid-data [nil (not instance? java.lang.Number "Not a number")]}

resources/test.html where is it used?

I'm using part of your configuration for clojurescript.test tests configuration. I copied resources/test.html to my project, but it seems it's not being used anywhere, because if I remove the file, the tests run normally.

Working with ahead of time compilation?

Adding (:gen-class) to any file that requires schema.core seems to introduce issues that prevent it from compiling.

If this is indeed an issue, the workaround of course is to extract my schema.core functions from the aot file.

edit: Nevermind, I haven't yet figured out an issue I'm having.

CLJS Error's back.

Bad news. Seeing this again. I added in source maps this time, and here's what I'm seeing:

Uncaught TypeError: Object #<um> has no method 'xa' core.cljs:186
nn core.cljs:186
c stringbuffer.js:102
Zp timer.cljs:120
(anonymous function) timer.cljs:339

core.cljs is schema.core.

On line 339 of timer.cljs, I see:

(dommy/listen! [js/document :a.remove]
                 :click (fn [e] (delete-row! (get-target-attr e "data-rank"))))

and on line 120:

(ajax/POST (str "/races/" regatta-title "/save-times")
             {:params {"regatta-title" regatta-title
                       "event-numbers[]" (JSON/parse event-numbers)
                       "timing-state" new-state}
              :format :json
              :response-format :json
              :keywords? true
              :handler (fn [response]
                         (.log js/console "Autosave: " response))
              :error-handler (fn [{:keys [status status-text]}]
                               (.log js/console "Autosave: Error " status-text)) })

Strangely, none of these lines seems to touch schema... I'm on dommy 0.1.1. Seeing if 0.1.2 fixes this, for some reason.

defn breaks recur

attempting to evaluaate

(s/defn foo :- s/Int [a :- s/Int] (if (= a 0) (recur (inc a)) (* a 2)))

results in the error "Can only recur from tail position".

Supporting a key marked both as optional and required

Hello!

I'm programmatically generating schemas, and I end up with things like

{:a {:b s/Int} (s/optional-key :a) {:c s/Int}}

These don't validate structures like

 {:a {:b 30}}
 {:a {:b 20 :c 30}}

Shouldn't they? Is this something to be fixed? Perhaps I can contribute

with-fn-validation should set validation flag to previous value instead of false?

In our development environment we turn on schema checking for everything via

(schema.core/set-fn-validation! true)

In production this is obviously not feasible, only for certain parts. For that we do

(with-fn-validation 
   ...important code here...
)

Because with-fn-validation does (schema.core/set-fn-validation! false) after execution [1] this causes unexpected results in development; e.g. you think something is correct because of the schemas, but after a while you realise the schemas weren't active.

Is there a reason we cannot/shouldn't use the previous value?

[1] https://github.com/Prismatic/schema/blob/master/src/clj/schema/macros.clj#L541

Schema won't work with CLJS

I have a .cljx file with a bunch of schema.macro/defrecords. It looks as follows:

(ns codenotes.types
  (:require
  #+cljs [schema.utils]
  #+cljs [clojure.data]
  [schema.core :as sc])
  #+clj (:require [schema.macros :as sm])
  #+cljs (:require-macros
          [potemkin]
          [schema.macros :as sm]))

…

(sm/defrecord NotesUpdateSC
    [notes :- [Note]])

When I try to compile it with CLJS, I get something like

WARNING: Use of undeclared Var codenotes.types/NotesUpdateSC at line 69 src/cljs/codenotes/types.cljs
WARNING: Use of undeclared Var codenotes.types/NotesUpdateSC at line 69 src/cljs/codenotes/types.cljs
WARNING: Use of undeclared Var codenotes.types/Note at line 69 src/cljs/codenotes/types.cljs
WARNING: Use of undeclared Var codenotes.types/NotesUpdateSC at line 69 src/cljs/codenotes/types.cljs
WARNING: Use of undeclared Var codenotes.types/NotesUpdateSC at line 69 src/cljs/codenotes/types.cljs
WARNING: Use of undeclared Var codenotes.types/NotesUpdateSC at line 69 src/cljs/codenotes/types.cljs
WARNING: Use of undeclared Var codenotes.types/NotesUpdateSC at line 69 src/cljs/codenotes/types.cljs

for every defrecord in this file. When I try to run resulting file, JS fails with error Uncaught TypeError: Cannot set property 'schema$utils$schema' of undefined.

According to jonasen in #clojure, the error is reproducible.

An excerpt from my project.clj:

[org.clojure/clojure "1.5.1"]
[org.clojure/clojurescript "0.0-1934"]
[prismatic/schema "0.1.6"]

Custom new record fn?

It might be nice to customize the new record fn when defining a new record, such that it has type annotations and could be used directly with with-fn-validation. For example:

(sm/defrecord Person [name :- String, age :- Int])

; Would throw an error
(with-fn-validation (->Person :billy 35.5))

I just ran into a case where I needed this. I ended up creating a wrapper function, which seems slightly inelegant.

David

Composable validation and transformation pipelines

I'm interested in composable pipelines for processing messages, where each step is a pure function that can be chained together.

I'm think of something like (pseudocode):

(-> message
  (validate my-schema)
  (check-assertion some-condition?)
  (rename-key :from :to)
  (with-key :sub-message
     (validate sub-schema))
  (convert-to-json-string))

Are there any plans to support things like this in Prismatic/schema? Or is this definitely out of scope?

Inconsistent results for different collection types and schema types vs. explicit predicates

I'm trying to use schema to validate collections of reifys.

Here's a schema, an equivelent (as far as I can tell) predicate function, and a function invoking both on different concrete collection types:

(def s-result
  (s/either
   ;;A file
   (s/both
    IHazFilename
    ICountedBytes
    IWriteToByteBufferFromChunkStore)

   ;;A directory
   (s/both
    IHazFilename
    clojure.lang.ILookup
    IHazKeys)))

(defn result?
  [x]
  (or
   (and
    (satisfies? IHazFilename x)
    (satisfies? ICountedBytes x)
    (satisfies? IWriteToByteBufferFromChunkStore x))

   (and
    (satisfies? IHazFilename x)
    (instance? clojure.lang.ILookup x)
    (satisfies? IHazKeys x))))

(s/defn ^:always-validate q
  [db query]
  (let [results (-q db query)]

    (prn (every? result? results))

    (prn (s/check [(s/pred result?)] results))
    (prn (s/check [(s/pred result?)] (vector results)))
    (prn (s/check #{(s/pred result?)} (set results)))

    (prn (s/check [s-result] results))
    (prn (s/check [s-result] (vector results)))
    (prn (s/check #{s-result} (set results)))

    results))

The results are somewhat bewildering:

true
nil
[(not (#<protocols$result_QMARK_ com.keminglabs.rucksack.protocols$result_QMARK_@7c1b6134> a-clojure.lang.LazySeq))]
nil
[(not (every? (check % a-com.keminglabs.rucksack.metadata_storage.datomic$entity$reify__69200) schemas)) (not (every? (check % a-com.keminglabs.rucksack.metadata_storage.datomic$entity$reify__69200) schemas)) (not (every? (check % a-com.keminglabs.rucksack.metadata_storage.datomic$entity$reify__69204) schemas))]
[(not (every? (check % a-clojure.lang.LazySeq) schemas))]
#{(not (every? (check % a-com.keminglabs.rucksack.metadata_storage.datomic$entity$reify__69200) schemas)) (not (every? (check % a-com.keminglabs.rucksack.metadata_storage.datomic$entity$reify__69204) schemas)) (not (every? (check % a-com.keminglabs.rucksack.metadata_storage.datomic$entity$reify__69200) schemas))}

Maybe I am misunderstanding the appropriate place to use [] vs #{} for verifying that all of the elements of a collection satisfy a schema?

This is all with schema 0.1.6 on java 1.7 and clojure 1.5.1

How to validate String map-keys?

As the docs show, you can specify Keyword map-keys:

(s/validate {:foo s/String
             s/Keyword s/String}
            {:foo "bar"
             :abc "def"})
;=> nil

But I'm having trouble doing the same with String map-keys:

(s/validate {"foo" s/String
             s/String s/String}
            {"foo" "bar"
             "abc" "def"})
;=> RuntimeException More than one non-optional/required 
; key schemata: ["foo" java.lang.String]  schema.utils/error! 
; (utils.clj:22)

I can use s/eq to get a single String key to validate:

(s/validate {(s/eq "foo") s/String} {"foo" "bar"})
;=> nil

But it will throw the same exception when I try to do more:

(s/validate {(s/eq "foo") s/String
             s/String s/String}
            {"foo" "bar"
             "abc" "def"})
;=> RuntimeException

schema.core/either error reporting isn't really useful

Here is a definition of schema:

(def SCRecords (sc/either NotesUpdateSC ReposUpdateSC))

And here is a check result:

(sc/check t/SCRecords (t/->ReposUpdateSC test-repos-list))
(not (every? (check % a-codenotes.types.ReposUpdateSC) schemas))

As you can see, this error report isn't really useful, especially when compared to this one:

(sc/check codenotes.types.ReposUpdateSC (t/->ReposUpdateSC test-repos-list))
{:repos [{:commit-hash (not (instance? java.lang.String nil)), :commit-text (not (instance? java.lang.String nil))} nil]}

"name-with-attributes" feature in schema.core

I wanted to get your thoughts before submitting a pull req. How do you guys feel about a name-with-attributes implementation in schema?

(`name-with-attributes is a function in clojure.tools.macro that lets you sort out all of the initial arguments in macro definitions. This would let users build schema-aware custom binding forms.)

Potential example usage, taken from a place in Cascalog where I use name-with-attributes:

(defmacro defop
  "Defines a flow operation."
  [f-name & tail]
  (let [[f-name args body] (schema.core/name-with-attributes f-name &env tail)]
    `(defn ~f-name
       {:arglists '([~'flow ~@args])}
       [flow# ~@args]
       (add-op flow# ~@body))))

(defop each
  "Accepts a flow, a function from result fields => cascading
  Function, input fields and output fields and returns a new flow."
  [f from-fields :- [String] to-fields :- [String]]
  (let [from-fields (fields from-fields)
        to-fields   (fields to-fields)]
    (fn [pipe]
      (Each. pipe
             from-fields
             (f to-fields)
             (default-output from-fields to-fields)))))

Doc Bug

The following code:

(s/defn multi-arity :- Int
  ([x :- Int] (multi-arity x 0))
  ([x :- Int, y :- Int] (+ x y)))

(doc multi-arity)

Produces this docstring:

([x] [x y])
  Inputs: clojure.lang.LazySeq@1f07eb92
  Returns: Int

Issue is with "Inputs: ..."

add nesting to readme

The ability to nest collections is very nice, I'd suggest adding an example to the readme.

(s/check {:a {:b s/String
              :c s/Int}
          :d [{:e s/Keyword
               :f [s/Number]
               :g {:h s/Keyword
                   :i s/Keyword}}]}

         {:a {:b "abc"
              :c 123}
          :d [{:e :bc
               :f [12.2 13]
               :g {:h :foo
                   :i :bar}}
              {:e :bc
               :f [12 13.3]
               :g {:h :foo
                   :i :bar}}]})

`with-fn-validation` persists beyond block

(ns schema-test.core
  (:require [schema.core :as s]
            [schema.utils]))

(s/defn add :- s/Number
  [a :- s/Number
   b :- s/Number]
  (+ a b))

(add 1 "2")  ;=> ClassCastException: String can't cast to Number.

(s/with-fn-validation
  (add 1 "2")) ;=> RuntimeException: Input doesn't match schema.

(add 1 "2") ;=> RuntimeException: Input doesn't match schema.

(.set_cell schema.utils/use-fn-validation false)

(add 1 "2")  ;=> ClassCastException: String can't cast to Number.

I'm not experienced enough to understand why .set_cell use-fn-validation false doesn't seem to work within a let body nor submit this issue confidently, but the code example above conflicts with my understanding and expectation of with-fn-validation.

I expected the second invocation of (add 1 "2") to throw a ClassCastException.

https://github.com/Prismatic/schema/blob/22873d6348c71207ef52c9e8441f0cd0bb17b108/src/clj/schema/macros.clj#L436

Order seems to matter when using both

First a helper schema:

(def not-nil (pred (complement nil?)))

This works fine:

=> (validate (both not-nil [Int]) nil)
ExceptionInfo Value does not match schema: (not (#<core$complement$fn__4048 clojure.core$complement$fn__4048@3d722bd> nil))  schema.core/validate (core.clj:165)

But this doesn’t:

=> (validate (both [Int] not-nil) nil)
nil

This is counterintuitive.

looking for some sort of "s/optional-seq"

Maybe I'm just missing it, but I'm interested in having "optional lists" such that either of these could be valid:

(s/check
  {:foo [s/Keyword]}
  {:foo [:abc]})

(s/check
  {:foo [s/Keyword]}
  {:foo :abc})

Maybe something like:

(s/check
  {:foo (s/optional-seq s/Keyword)}
  {:foo [:abc]})

(s/check
  {:foo (s/optional-seq s/Keyword)}
  {:foo :abc})

defn doesn't work with nontrivial binding forms

user> (require '[schema.core :as s])
nil
user> (s/defn foo :- s/Int [a :- s/Int] (* a 2)) ;; ok!
nil
user> (foo 2)  ;; ok!
4
user> (s/defn foo :- s/Int [{a :- s/Int :x}] (* a 2)) ;; I want to validate the :x key of this map
CompilerException java.lang.RuntimeException: Can't let qualified name: s/Int, compiling:(NO_SOURCE_PATH:1) 
user> (s/defn foo :- s/Int [{a :x :as m :- {:x s/Int}}] (* a 2)) ;; I want to extract the :x key and validate the whole map
CompilerException java.lang.Exception: Unsupported binding form: :-, compiling:(NO_SOURCE_PATH:1) 
user> 

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.