├── .gitignore ├── .gitmodules ├── .travis.yml ├── LICENSE ├── README.md ├── project.clj ├── src └── webjure │ └── json_schema │ ├── ref.clj │ ├── validator.clj │ └── validator │ ├── format.clj │ ├── macro.clj │ └── string.clj └── test ├── resources ├── address-and-phone-additional-properties.json ├── address-and-phone-city-and-code-missing.json ├── address-and-phone-null.json ├── address-and-phone-valid.json ├── address-and-phone.schema.json ├── format.schema.json ├── person-invalid.json ├── person-valid.json └── person.schema.json └── webjure └── json_schema ├── suite_test.clj ├── test_util.clj └── validator_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .lein* 3 | target 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/resources/JSON-Schema-Test-Suite"] 2 | path = test/resources/JSON-Schema-Test-Suite 3 | url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tatu Tarvainen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # json-schema validator 2 | 3 | Currently contains a usable JSON schema validator using cheshire to parse JSON. 4 | Supportes linked schemas with $ref and allows user to specify 5 | how linked URIs are loaded. 6 | 7 | [![Clojars Project](http://clojars.org/webjure/json-schema/latest-version.svg)](http://clojars.org/webjure/json-schema) 8 | 9 | [![Build Status](https://travis-ci.org/tatut/json-schema.svg?branch=master)](https://travis-ci.org/tatut/json-schema) 10 | 11 | ## Status 12 | 13 | The project is tested against [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) 14 | and passes most of the tests. 15 | 16 | The macro version has problems with recursive and huge schemas. 17 | A schema that later links to itself with a "#" pointer causes an ever expanding 18 | macro expansion to take place and fails. That case requires a rethink of the 19 | macro version. The macro may also fail when the generated function would exceed 20 | the JVM limit on allowed code in a single method (64k). 21 | 22 | Currently there are 47 tests with 658 assertions. 23 | See [suite_test.clj](https://github.com/tatut/json-schema/blob/master/test/webjure/json_schema/suite_test.clj#L16) for 24 | a the list of tests in the JSON schema test suite that are skipped. 25 | 26 | ## Usage 27 | 28 | The function version (runtime loading of schema): 29 | 30 | ```clojure 31 | (ns my.app 32 | (:require [webjure.json-schema.validator :refer [validate]] 33 | [cheshire.core :as cheshire])) 34 | 35 | ;;; then in some function 36 | (validate (cheshire/parse-string json-schema) 37 | (cheshire/parse-string json-data)) 38 | 39 | ``` 40 | 41 | Macro version loads and parses the schema and generates the validation function at compile time. 42 | The returned errors are exactly the same as in the runtime version. 43 | 44 | ```clojure 45 | (ns my.app 46 | (:require [webjure.json-schema.validator.macro :refer [make-validator]] 47 | [cheshire.core :as cheshire])) 48 | 49 | (def my-schema-validator 50 | (make-validator (cheshire/parse-string json-schema) {})) 51 | 52 | ;; Then in some function 53 | (my-schema-validator (cheshire/parse-string json-data)) 54 | ``` 55 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject webjure/json-schema "20180613-SNAPSHOT" 2 | :description "JSON schema validator" 3 | :url "https://github.com/tatut/json-schema" 4 | :license {:name "MIT License" 5 | :url "http://www.opensource.org/licenses/mit-license.php"} 6 | :dependencies [[org.clojure/clojure "1.8.0"] 7 | [cheshire "5.6.1"] 8 | [clj-time "0.11.0"]] 9 | :source-paths ["src"] 10 | :test-paths ["test"]) 11 | -------------------------------------------------------------------------------- /src/webjure/json_schema/ref.clj: -------------------------------------------------------------------------------- 1 | (ns webjure.json-schema.ref 2 | (:require [cheshire.core :as cheshire] 3 | [clojure.java.io :as io] 4 | [clojure.string :as str]) 5 | (:import (java.net URI URISyntaxException))) 6 | 7 | (defn initialize-id-path 8 | "Recursively set the id path in the schema for subsequent reference loading." 9 | ([schema] (initialize-id-path "" schema)) 10 | ([prefix schema] 11 | (let [id-path (str prefix (get schema "id"))] 12 | (with-meta (reduce-kv 13 | (fn [m k v] 14 | (assoc m k 15 | (if (map? v) 16 | (initialize-id-path id-path v) 17 | v))) 18 | {} 19 | schema) 20 | {::id-path id-path})))) 21 | 22 | (defn resolve-ref [uri] 23 | (-> uri 24 | slurp 25 | cheshire/parse-string 26 | initialize-id-path)) 27 | 28 | (def pointer-pattern #"^#/.*") 29 | (defn pointer? [ref] 30 | (re-matches pointer-pattern ref)) 31 | 32 | (defn- unescape [c] 33 | (-> c 34 | (str/replace #"~1" "/") 35 | (str/replace #"~0" "~") 36 | java.net.URLDecoder/decode)) 37 | 38 | (defn dereference-pointer [root-schema ref] 39 | (let [components (-> ref 40 | (subs 2 (count ref)) 41 | (str/split #"/")) 42 | key-path (mapv (fn [c] 43 | (let [c (unescape c)] 44 | (if (re-matches #"\d+" c) 45 | (Long/parseLong c) 46 | c))) 47 | components)] 48 | ;;(println "REF: " ref " => key path: " (pr-str key-path) " root-schema: " root-schema) 49 | (get-in root-schema key-path))) 50 | 51 | 52 | (defn- ref? [schema] 53 | (contains? schema "$ref")) 54 | 55 | (defn- absolute? [uri] 56 | (try 57 | (.isAbsolute (URI. uri)) 58 | (catch URISyntaxException _ 59 | false))) 60 | 61 | (defn resolve-schema [schema options] 62 | (loop [schema schema 63 | root-schema (:root-schema options)] 64 | (let [ref (get schema "$ref")] 65 | ;;(println "RESOLVE-SCHEMA, ref: " (pr-str ref) ", schema: " (pr-str schema) ", root: " (pr-str root-schema)) 66 | (cond 67 | 68 | ;; No reference, return as is 69 | (nil? ref) 70 | (do #_(println " => " schema) schema) 71 | 72 | ;; Reference to whole document 73 | (= ref "#") 74 | (do #_(println " => " root-schema) root-schema) 75 | 76 | ;; Pointer, dereference it 77 | (pointer? ref) 78 | (recur (dereference-pointer root-schema ref) root-schema) 79 | 80 | ;; URI, try to load it 81 | :default 82 | (let [id-path (::id-path (meta schema)) 83 | uri (if (absolute? ref) 84 | ref 85 | (str id-path ref)) 86 | fragment (.getFragment (URI. uri)) 87 | resolver (or (:ref-resolver options) 88 | resolve-ref) 89 | referenced-schema (resolver uri) 90 | referenced-schema (if (str/blank? fragment) 91 | referenced-schema 92 | ;; If there was a fragment treat it as a 93 | ;; $ref within the loaded document 94 | (resolve-schema {"$ref" (str "#" fragment)} 95 | {:root-schema referenced-schema}))] 96 | (if-not referenced-schema 97 | ;; Throw exception if schema can't be loaded 98 | (throw (IllegalArgumentException. 99 | (str "Unable to resolve referenced schema: " ref))) 100 | (recur referenced-schema referenced-schema))))))) 101 | 102 | (defn root-schema [{root :root-schema :as options} schema] 103 | (if root 104 | options 105 | (assoc options 106 | :root-schema (or (resolve-schema schema options) 107 | schema)))) 108 | -------------------------------------------------------------------------------- /src/webjure/json_schema/validator.clj: -------------------------------------------------------------------------------- 1 | (ns webjure.json-schema.validator 2 | "Validator for JSON schema for draft 4" 3 | (:require [clojure.string :as str] 4 | [webjure.json-schema.ref :as ref :refer [resolve-ref resolve-schema]] 5 | [webjure.json-schema.validator 6 | [format :as format] 7 | [string :as string]])) 8 | 9 | (declare validate) 10 | 11 | (defn validate-type 12 | "Check one of the seven JSON schema core types" 13 | [{type "type"} data _] 14 | 15 | (when type 16 | (let [check-type #(case % 17 | "array" (sequential? data) 18 | "boolean" (instance? Boolean data) 19 | "integer" (or (instance? Integer data) 20 | (instance? Long data) 21 | (instance? java.math.BigInteger data)) 22 | "number" (number? data) 23 | "null" (nil? data) 24 | "object" (map? data) 25 | "string" (string? data) 26 | nil nil)] 27 | 28 | (when-not (if (sequential? type) 29 | (some check-type type) 30 | (check-type type)) 31 | {:error :wrong-type 32 | :expected (if (sequential? type) 33 | (into #{} 34 | (map keyword) 35 | type) 36 | (keyword type)) 37 | :data data})))) 38 | 39 | 40 | (defn validate-number-bounds [{min "minimum" max "maximum" 41 | exclusive-min "exclusiveMinimum" 42 | exclusive-max "exclusiveMaximum" 43 | multiple-of "multipleOf" :as schema} 44 | data _] 45 | (when (or min max multiple-of) 46 | (cond 47 | (not (number? data)) 48 | nil 49 | 50 | (and min exclusive-min 51 | (<= data min)) 52 | {:error :out-of-bounds 53 | :data data 54 | :minimum min 55 | :exclusive true} 56 | 57 | (and min (not exclusive-min) 58 | (< data min)) 59 | {:error :out-of-bounds 60 | :data data 61 | :minimum min 62 | :exclusive false} 63 | 64 | 65 | (and max exclusive-max 66 | (>= data max)) 67 | {:error :out-of-bounds 68 | :data data 69 | :maximum max 70 | :exclusive true} 71 | 72 | (and max (not exclusive-max) 73 | (> data max)) 74 | {:error :out-of-bounds 75 | :data data 76 | :maximum max 77 | :exclusive false} 78 | 79 | 80 | (and multiple-of 81 | (not (or (zero? data) 82 | (let [d (/ data multiple-of)] 83 | (or (integer? d) 84 | (= (Math/floor d) d)))))) 85 | {:error :not-multiple-of 86 | :data data 87 | :expected-multiple-of multiple-of} 88 | 89 | :default 90 | nil))) 91 | 92 | (defn validate-string-length [{min "minLength" max "maxLength" :as schema} data _] 93 | (when (or min max) 94 | (cond 95 | (not (string? data)) 96 | nil 97 | 98 | (and min 99 | (< (string/length data) min)) 100 | {:error :string-too-short 101 | :minimum-length min 102 | :data data} 103 | 104 | (and max 105 | (> (string/length data) max)) 106 | {:error :string-too-long 107 | :maximum-length max 108 | :data data} 109 | 110 | :default 111 | nil))) 112 | 113 | (defn validate-string-pattern [{pattern "pattern"} data _] 114 | (when pattern 115 | (if (or (not (string? data)) 116 | (re-find (re-pattern pattern) data)) 117 | nil 118 | {:error :string-does-not-match-pattern 119 | :pattern pattern 120 | :data data}))) 121 | 122 | (defn validate-string-format [{format "format"} data 123 | {lax-date-time-format? :lax-date-time-format?}] 124 | (when (and format data) 125 | ((case format 126 | "date-time" (if lax-date-time-format? 127 | format/validate-lax-date-time 128 | format/validate-date-time) 129 | "hostname" format/validate-hostname 130 | "ipv4" format/validate-ipv4 131 | "ipv6" format/validate-ipv6 132 | "uri" format/validate-uri 133 | "email" format/validate-email 134 | (do 135 | (println "WARNING: Unsupported format: " format) 136 | (constantly nil))) 137 | data))) 138 | 139 | (defn validate-properties [{properties "properties" 140 | pattern-properties "patternProperties" 141 | additional-properties "additionalProperties" 142 | :as schema} data options] 143 | 144 | (when (or properties additional-properties pattern-properties) 145 | (let [properties (or properties {}) 146 | additional-properties (if (nil? additional-properties) {} additional-properties) 147 | required (if (:draft3-required options) 148 | ;; Draft 3 required is an attribute of the property schema 149 | (into #{} 150 | (for [[property-name property-schema] properties 151 | :when (get property-schema "required")] 152 | property-name)) 153 | 154 | ;; Draft 4 has separate required attribute with a list of property names 155 | (into #{} 156 | (get schema "required"))) 157 | property-names (into #{} (map first properties))] 158 | (if-not (map? data) 159 | nil 160 | (let [property-errors 161 | (as-> {} property-errors 162 | 163 | ;; Check required props 164 | (merge property-errors 165 | (into {} 166 | (keep (fn [p] 167 | (when-not (contains? data p) 168 | [p {:error :missing-property}]))) 169 | required)) 170 | 171 | ;; Property validations 172 | (merge property-errors 173 | (into {} 174 | (keep (fn [[property-name property-schema]] 175 | (let [v (get data property-name ::not-found)] 176 | (when (not= ::not-found v) 177 | ;; not found values for required fields are checked earlier 178 | (when-let [e (validate property-schema v options)] 179 | [property-name e]))))) 180 | properties)) 181 | 182 | ;; Validate pattern properties 183 | (merge property-errors 184 | (into {} 185 | (keep 186 | (fn [[pattern schema]] 187 | (let [invalid-pp 188 | (keep (fn [name] 189 | (when (re-find (re-pattern pattern) name) 190 | (let [v (get data name)] 191 | (validate schema v options)))) 192 | (keys data))] 193 | (when-not (empty? invalid-pp) 194 | [pattern {:error :invalid-pattern-properties 195 | :pattern pattern 196 | :schema schema 197 | :properties (into #{} invalid-pp)}])))) 198 | pattern-properties)))] 199 | (if-not (empty? property-errors) 200 | {:error :properties 201 | :data data 202 | :properties property-errors} 203 | 204 | (let [extra-properties (when-not (#{true {}} additional-properties) 205 | (as-> (keys data) props 206 | (remove property-names props) 207 | (if pattern-properties 208 | (remove 209 | (fn [p] 210 | (some #(re-find % p) 211 | (map re-pattern 212 | (keys pattern-properties)))) 213 | props) 214 | props) 215 | (into #{} props)))] 216 | (cond 217 | ;; No additional properties allowed, signal error if there are any 218 | (false? additional-properties) 219 | (if-not (empty? extra-properties) 220 | ;; We have properties outside the schema, error 221 | {:error :additional-properties 222 | :property-names extra-properties} 223 | 224 | ;; No errors 225 | nil) 226 | 227 | 228 | ;; Additional properties is a schema, check all extra properties 229 | ;; against schema 230 | (and (map? additional-properties) (not= {} additional-properties)) 231 | (let [invalid-additional-properties 232 | (into {} 233 | (keep (fn [prop] 234 | (let [v (get data prop) 235 | e (validate additional-properties v options)] 236 | (when e 237 | [prop e])))) 238 | extra-properties)] 239 | (when-not (empty? invalid-additional-properties) 240 | {:error :invalid-additional-properties 241 | :invalid-additional-properties invalid-additional-properties 242 | :data data})) 243 | 244 | :default 245 | nil)))))))) 246 | 247 | (defn validate-property-count [{min "minProperties" max "maxProperties" :as schema} data _] 248 | (when (or min max) 249 | (cond 250 | (not (map? data)) 251 | nil 252 | 253 | (and min (< (count data) min)) 254 | {:error :too-few-properties 255 | :minimum-properties min 256 | :data data} 257 | 258 | (and max (> (count data) max)) 259 | {:error :too-many-properties 260 | :maximum-properties max 261 | :data data} 262 | 263 | :default 264 | nil))) 265 | 266 | (defn validate-enum-value 267 | [{enum "enum"} data _] 268 | (when-let [allowed-values (and enum (into #{} enum))] 269 | (when-not (allowed-values data) 270 | {:error :invalid-enum-value 271 | :data data 272 | :allowed-values allowed-values}))) 273 | 274 | (defn validate-array-items [{item-schema "items" 275 | additional-items "additionalItems" :as schema} data options] 276 | (when item-schema 277 | (let [c (count data)] 278 | (if-not (sequential? data) 279 | nil 280 | 281 | 282 | (cond 283 | ;; Schema is a map: validate all items against it 284 | (map? item-schema) 285 | (loop [errors [] 286 | i 0] 287 | (if (= i c) 288 | (when-not (empty? errors) 289 | {:error :array-items 290 | :data data 291 | :items errors}) 292 | (let [item (nth data i) 293 | item-error 294 | (if (and (map? item-schema) (item-schema "enum")) 295 | (validate-enum-value item-schema item options) 296 | (validate item-schema item options))] 297 | (recur (if item-error 298 | (conj errors (assoc item-error 299 | :position i)) 300 | errors) 301 | (inc i))))) 302 | 303 | ;; Schema is an array, validate each index with its own schema 304 | (sequential? item-schema) 305 | (or #_(and (< c (count item-schema)) 306 | {:error :too-few-items 307 | :expected-item-count (count item-schema) 308 | :actual-item-count c 309 | :data data}) 310 | 311 | (first 312 | (let [items (count data)] 313 | (for [i (range (count item-schema)) 314 | :let [e (when (> items i) 315 | (validate (nth item-schema i) (nth data i) options))] 316 | :when e] 317 | e))) 318 | 319 | ;; If additional items is false, don't allow more items 320 | (and (false? additional-items) 321 | (> (count data) (count item-schema)) 322 | {:error :additional-items-not-allowed 323 | :additional-items (drop (count item-schema) data) 324 | :data data}) 325 | 326 | ;; If additional items is a schema, check each against it 327 | (and (map? additional-items) 328 | (some (fn [item] 329 | (when-let [e (validate additional-items item options)] 330 | {:error :additional-item-does-not-match-schema 331 | :item-error e 332 | :data data})) 333 | (drop (count item-schema) data))))))))) 334 | 335 | (defn validate-array-item-count [{min-items "minItems" max-items "maxItems"} data options] 336 | (when (or min-items max-items) 337 | (if-not (sequential? data) 338 | nil 339 | (let [items (count data)] 340 | (cond 341 | (and min-items (> min-items items)) 342 | {:error :wrong-number-of-elements 343 | :minimum min-items :actual items} 344 | 345 | (and max-items (< max-items items)) 346 | {:error :wrong-number-of-elements 347 | :maximum max-items :actual items} 348 | 349 | :default 350 | nil))))) 351 | 352 | (defn validate-array-unique-items [{unique-items "uniqueItems"} data _] 353 | (when unique-items 354 | (if-not (sequential? data) 355 | nil 356 | (let [c (count data)] 357 | (loop [seen #{} 358 | duplicates #{} 359 | i 0] 360 | (if (= i c) 361 | (when-not (empty? duplicates) 362 | {:error :duplicate-items-not-allowed 363 | :duplicates duplicates}) 364 | (let [item (nth data i)] 365 | (recur (conj seen item) 366 | (if (seen item) 367 | (conj duplicates item) 368 | duplicates) 369 | (inc i))))))))) 370 | 371 | (defn validate-not [{schema "not"} data options] 372 | (when schema 373 | (let [e (validate schema data options)] 374 | (when (nil? e) 375 | {:error :should-not-match 376 | :schema schema 377 | :data data})))) 378 | 379 | (defn validate-all-of 380 | "Validate match against all of the given schemas." 381 | [{all-of "allOf"} data options] 382 | (when-not (empty? all-of) 383 | (when-not (every? #(nil? (validate % data options)) all-of) 384 | {:error :does-not-match-all-of 385 | :all-of all-of 386 | :data data}))) 387 | 388 | (defn validate-any-of 389 | "Validate match against any of the given schemas." 390 | [{any-of "anyOf"} data options] 391 | (when-not (empty? any-of) 392 | (let [errors (mapv #(validate % data options) any-of)] 393 | (when-not (some nil? errors) 394 | {:error :does-not-match-any-of 395 | :any-of any-of 396 | :data data 397 | :errors errors})))) 398 | 399 | (defn validate-one-of 400 | "Validate match against one (and only one) of the given schemas." 401 | [{one-of "oneOf"} data options] 402 | (when-not (empty? one-of) 403 | (let [c (reduce (fn [c schema] 404 | (if (nil? (validate schema data options)) 405 | (inc c) 406 | c)) 407 | 0 one-of)] 408 | (when-not (= 1 c) 409 | {:error :item-does-not-match-exactly-one-schema 410 | :data data 411 | :matched-schemas c})))) 412 | 413 | (defn validate-dependencies [{dependencies "dependencies" :as schema} data options] 414 | (when (and (not (empty? dependencies)) 415 | (map? data)) 416 | (some (fn [[property schema-or-properties]] 417 | (when (and (contains? data property) 418 | (if (map? schema-or-properties) 419 | (validate schema-or-properties data options) 420 | (not (every? #(contains? data %) schema-or-properties)))) 421 | {:error :dependency-mismatch 422 | :dependency {property schema-or-properties} 423 | :data data})) 424 | dependencies))) 425 | 426 | (def validations [#'validate-not #'validate-all-of #'validate-any-of #'validate-one-of 427 | #'validate-dependencies 428 | #'validate-type 429 | #'validate-enum-value 430 | #'validate-number-bounds 431 | #'validate-string-length #'validate-string-pattern validate-string-format 432 | #'validate-properties #'validate-property-count 433 | #'validate-array-items #'validate-array-item-count #'validate-array-unique-items]) 434 | 435 | (defn validate 436 | "Validate data against the given schema. 437 | 438 | An map of options can be given that supports the keys: 439 | :ref-resolver Function for loading referenced schemas. Takes in 440 | the schema URI and must return the schema parsed form. 441 | Default just tries to read it as a file via slurp and parse. 442 | 443 | :draft3-required when set to true, support draft3 style required (in property definition), 444 | defaults to false 445 | 446 | :lax-date-time-format? when set to true, allow more variation in date format, 447 | normally only strict RFC3339 dates are valid" 448 | ([schema data] 449 | (validate schema data {})) 450 | ([schema data options] 451 | (let [options (-> options 452 | (ref/root-schema schema) 453 | (assoc :ref-resolver (or (:ref-resolver options) 454 | (memoize ref/resolve-ref)))) 455 | schema (resolve-schema schema options) 456 | definitions (get schema "definitions")] 457 | (some #(% schema data options) validations)))) 458 | -------------------------------------------------------------------------------- /src/webjure/json_schema/validator/format.clj: -------------------------------------------------------------------------------- 1 | (ns webjure.json-schema.validator.format 2 | (:require [clj-time.format :as time-format] 3 | [clj-time.coerce :as time-coerce])) 4 | 5 | (def rfc3339-formatter (time-format/formatters :date-time)) 6 | 7 | (def hostname-pattern 8 | ;; Courtesy of StackOverflow http://stackoverflow.com/a/1420225 9 | #"^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\.?$") 10 | 11 | (def ipv4-pattern 12 | #"^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$") 13 | 14 | (def ipv6-pattern 15 | #"^[:a-f0-9]+$") 16 | 17 | (def email-pattern 18 | ;; I know, this isn't too permissive. This just checks 19 | ;; that the email has an @ character (exactly one) and something 20 | ;; on both sides of it. 21 | #"^[^@]+@[^@]+$") 22 | 23 | (defn validate-date-time [d] 24 | (try 25 | (time-format/parse rfc3339-formatter (str d)) 26 | nil 27 | (catch Exception _ 28 | {:error :wrong-format :expected :date-time :data d}))) 29 | 30 | (defn validate-lax-date-time [d] 31 | (when (nil? (time-coerce/from-string (str d))) 32 | {:error :wrong-format :expected :date-time :data d})) 33 | 34 | (defn validate-hostname [d] 35 | (if (re-matches hostname-pattern (str d)) 36 | nil 37 | {:error :wrong-format :expected :hostname :data d})) 38 | 39 | (defn validate-ipv4 [d] 40 | (let [[ip & parts] (re-matches ipv4-pattern (str d))] 41 | (if (and ip 42 | (every? #(<= 0 (Integer/parseInt %) 255) parts)) 43 | nil 44 | {:error :wrong-format :expected :ipv4 :data d}))) 45 | 46 | ;; Check that the string contains only the allowed characters 47 | ;; and contains an ':' character. Then try to parse with Java 48 | ;; Inet6Address class. 49 | (defn validate-ipv6 [d] 50 | (let [d (str d)] 51 | (if (and (re-matches ipv6-pattern d) 52 | (.contains d ":") 53 | (try 54 | (java.net.Inet6Address/getByName d) 55 | true 56 | (catch Exception e# 57 | false))) 58 | nil 59 | {:error :wrong-format :expected :ipv6 :data d}))) 60 | 61 | (defn validate-uri [d] 62 | (let [d (str d)] 63 | (if-not (and (.contains d ":") 64 | (try 65 | (java.net.URI. d) 66 | true 67 | (catch java.net.URISyntaxException e# 68 | false))) 69 | {:error :wrong-format :expected :uri :data d} 70 | nil))) 71 | 72 | (defn validate-email [d] 73 | (if-not (re-matches email-pattern (str d)) 74 | {:error :wrong-format :expected :email :data d} 75 | nil)) 76 | -------------------------------------------------------------------------------- /src/webjure/json_schema/validator/macro.clj: -------------------------------------------------------------------------------- 1 | (ns webjure.json-schema.validator.macro 2 | "Macro version of validator. Loads and parses schema at compile time and 3 | emits code to check data for validity." 4 | (:require [webjure.json-schema.ref :refer [resolve-schema resolve-ref]] 5 | [webjure.json-schema.validator.string :as string] 6 | [webjure.json-schema.validator.format :as format] 7 | [clojure.string :as str] 8 | [webjure.json-schema.ref :as ref])) 9 | 10 | 11 | (declare validate) 12 | 13 | ;; Below are the functions that emit validations. 14 | ;; They all have the same signature: they take a schema, 15 | ;; the symbol for the data, error and ok expansion functions 16 | ;; and the options map. They return nil if there is no check 17 | ;; to be done for this type or an expansion for the check. 18 | 19 | (defn validate-type 20 | "Check one of the seven JSON schema core types" 21 | [{type "type"} data-sym error ok _] 22 | 23 | (when type 24 | (let [expand-type #(case % 25 | "array" `(sequential? ~data-sym) 26 | "boolean" `(instance? Boolean ~data-sym) 27 | "integer" `(or (instance? Integer ~data-sym) 28 | (instance? Long ~data-sym) 29 | (instance? java.math.BigInteger ~data-sym)) 30 | "number" `(number? ~data-sym) 31 | "null" `(nil? ~data-sym) 32 | "object" `(map? ~data-sym) 33 | "string" `(string? ~data-sym) 34 | nil nil) 35 | ] 36 | (let [e (gensym "ERROR")] 37 | `(if ~(if (sequential? type) 38 | `(or ~@(map expand-type type)) 39 | (expand-type type)) 40 | ~(ok) 41 | (let [~e {:error :wrong-type 42 | :expected ~(if (sequential? type) 43 | (into #{} 44 | (map keyword) 45 | type) 46 | (keyword type)) 47 | :data ~data-sym}] 48 | ~(error e))))))) 49 | 50 | (defn type-is? 51 | "Check if a type validation is taking place. This can be used to 52 | elide instance checks in later validations." 53 | [{type "type"} & types] 54 | (let [types (into #{} types)] 55 | (and type 56 | (not (sequential? type)) ;; either type, can't be sure 57 | (types type)))) 58 | 59 | (defn validate-number-bounds [{min "minimum" max "maximum" 60 | exclusive-min "exclusiveMinimum" 61 | exclusive-max "exclusiveMaximum" 62 | multiple-of "multipleOf" :as schema} 63 | data error ok _] 64 | (when (or min max multiple-of) 65 | (let [e (gensym "E")] 66 | `(cond 67 | ;; If no type check has been done, add type check that 68 | ;; skips non-number types 69 | ~@(when-not (type-is? schema "integer" "number") 70 | [`(not (number? ~data)) 71 | (ok)]) 72 | 73 | ~@(when (and min exclusive-min) 74 | [`(<= ~data ~min) 75 | `(let [~e {:error :out-of-bounds 76 | :data ~data 77 | :minimum ~min 78 | :exclusive true}] 79 | ~(error e))]) 80 | 81 | ~@(when (and min (not exclusive-min)) 82 | [`(< ~data ~min) 83 | `(let [~e {:error :out-of-bounds 84 | :data ~data 85 | :minimum ~min 86 | :exclusive false}] 87 | ~(error e))]) 88 | 89 | 90 | ~@(when (and max exclusive-max) 91 | [`(>= ~data ~max) 92 | `(let [~e {:error :out-of-bounds 93 | :data ~data 94 | :maximum ~max 95 | :exclusive true}] 96 | ~(error e))]) 97 | 98 | ~@(when (and max (not exclusive-max)) 99 | [`(> ~data ~max) 100 | `(let [~e {:error :out-of-bounds 101 | :data ~data 102 | :maximum ~max 103 | :exclusive false}] 104 | ~(error e))]) 105 | 106 | ~@(when multiple-of 107 | [`(not (or (zero? ~data) 108 | (let [d# (/ ~data ~multiple-of)] 109 | (or (integer? d#) 110 | (= (Math/floor d#) d#))))) 111 | `(let [~e {:error :not-multiple-of 112 | :data ~data 113 | :expected-multiple-of ~multiple-of}] 114 | ~(error e))]) 115 | 116 | :default 117 | ~(ok))))) 118 | 119 | (defn validate-string-length [{min "minLength" max "maxLength" :as schema} data error ok _] 120 | (when (or min max) 121 | (let [e (gensym "E")] 122 | `(cond 123 | ~@(when-not (type-is? schema "string") 124 | [`(not (string? ~data)) 125 | (ok)]) 126 | 127 | ~@(when min 128 | [`(< (string/length ~data) ~min) 129 | `(let [~e {:error :string-too-short 130 | :minimum-length ~min 131 | :data ~data}] 132 | ~(error e))]) 133 | ~@(when max 134 | [`(> (string/length ~data) ~max) 135 | `(let [~e {:error :string-too-long 136 | :maximum-length ~max 137 | :data ~data}] 138 | ~(error e))]) 139 | 140 | :default 141 | ~(ok))))) 142 | 143 | (defn validate-string-pattern [{pattern "pattern"} data error ok _] 144 | (when pattern 145 | (let [e (gensym "E")] 146 | `(if-not (string? ~data) 147 | ~(ok) 148 | (if (re-find ~(re-pattern pattern) ~data) 149 | ~(ok) 150 | (let [~e {:error :string-does-not-match-pattern 151 | :pattern ~pattern 152 | :data ~data}] 153 | ~(error e))))))) 154 | 155 | 156 | 157 | 158 | 159 | (defn validate-string-format [{format "format" :as schema} data error ok 160 | {lax-date-time-format? :lax-date-time-format?}] 161 | (when format 162 | (let [e (gensym "E")] 163 | `(if-let [~e (when ~data 164 | (~(case format 165 | "date-time" (if lax-date-time-format? 166 | 'webjure.json-schema.validator.format/validate-lax-date-time 167 | 'webjure.json-schema.validator.format/validate-date-time) 168 | "hostname" 'webjure.json-schema.validator.format/validate-hostname 169 | "ipv4" 'webjure.json-schema.validator.format/validate-ipv4 170 | "ipv6" 'webjure.json-schema.validator.format/validate-ipv6 171 | "uri" 'webjure.json-schema.validator.format/validate-uri 172 | "email" 'webjure.json-schema.validator.format/validate-email 173 | (do 174 | (println "WARNING: Unsupported format: " format " in schema: " schema) 175 | `(constantly nil))) 176 | ~data))] 177 | ~(error e) 178 | ~(ok))))) 179 | 180 | (defn validate-properties [{properties "properties" 181 | pattern-properties "patternProperties" 182 | additional-properties "additionalProperties" 183 | :as schema} data error ok options] 184 | (when (or properties additional-properties pattern-properties) 185 | (let [properties (or properties {}) 186 | additional-properties (if (nil? additional-properties) {} additional-properties) 187 | required (if (:draft3-required options) 188 | ;; Draft 3 required is an attribute of the property schema 189 | (into #{} 190 | (for [[property-name property-schema] properties 191 | :when (get property-schema "required")] 192 | property-name)) 193 | 194 | ;; Draft 4 has separate required attribute with a list of property names 195 | (into #{} 196 | (get schema "required"))) 197 | property-names (into #{} (map first properties)) 198 | e (gensym "E") 199 | property-errors (gensym "PROP-ERROR") 200 | v (gensym "V") 201 | d (gensym "DATA") 202 | props (gensym "PROPS") 203 | prop (gensym "PROP") 204 | extra-properties (gensym "EXTRA-PROPS") 205 | invalid-pp (gensym "INVALID-PP")] 206 | `(if-not (map? ~data) 207 | ~(ok) 208 | (let [~property-errors 209 | (as-> {} ~property-errors 210 | 211 | ;; Check required props 212 | ~@(for [p required] 213 | `(if-not (contains? ~data ~p) 214 | (assoc ~property-errors ~p {:error :missing-property}) 215 | ~property-errors)) 216 | 217 | ;; Property validations 218 | ~@(for [[property-name property-schema] properties 219 | :let [error (fn [error] 220 | `(assoc ~property-errors ~property-name ~error)) 221 | ok (constantly property-errors)]] 222 | `(let [~v (get ~data ~property-name ::not-found)] 223 | (if (= ::not-found ~v) 224 | ;; not found for required fields is checked earlier 225 | ~property-errors 226 | 227 | ;; validate property by type 228 | (if-let [~e ~(validate property-schema v options)] 229 | ~(error e) 230 | ~(ok))))) 231 | 232 | ;; Validate pattern properties 233 | ~@(for [[pattern schema] pattern-properties 234 | :let [error (fn [error] 235 | `(assoc ~property-errors ~pattern ~error)) 236 | ok (constantly property-errors)]] 237 | `(let [~invalid-pp (keep (fn [name#] 238 | (when (re-find ~(re-pattern pattern) name#) 239 | (let [~v (get ~data name#)] 240 | ~(validate schema v options)))) 241 | (keys ~data))] 242 | (if-not (empty? ~invalid-pp) 243 | (let [~e {:error :invalid-pattern-properties 244 | :pattern ~pattern 245 | :schema ~schema 246 | :properties (into #{} ~invalid-pp)}] 247 | ~(error e)) 248 | ~(ok)))) 249 | )] 250 | (if-not (empty? ~property-errors) 251 | (let [~e {:error :properties 252 | :data ~data 253 | :properties ~property-errors}] 254 | ~(error e)) 255 | 256 | (let [~extra-properties ~(when-not (#{true {}} additional-properties) 257 | `(as-> (keys ~data) ~props 258 | (remove ~property-names ~props) 259 | ~@(when pattern-properties 260 | [`(remove 261 | (fn [p#] 262 | (some #(re-find % p#) 263 | [~@(map re-pattern 264 | (keys pattern-properties))])) 265 | ~props)]) 266 | (into #{} ~props)))] 267 | ~(cond 268 | ;; No additional properties allowed, signal error if there are any 269 | (false? additional-properties) 270 | `(if-not (empty? ~extra-properties) 271 | ;; We have properties outside the schema, error 272 | (let [~e {:error :additional-properties 273 | :property-names ~extra-properties}] 274 | ~(error e)) 275 | 276 | ;; No errors 277 | ~(ok)) 278 | 279 | 280 | ;; Additional properties is a schema, check all extra properties 281 | ;; against schema 282 | (and (map? additional-properties) (not= {} additional-properties)) 283 | `(let [invalid-additional-properties# 284 | (into {} 285 | (keep (fn [~prop] 286 | (let [~v (get ~data ~prop) 287 | e# ~(validate additional-properties v options)] 288 | (when e# 289 | [~prop e#])))) 290 | ~extra-properties)] 291 | (if-not (empty? invalid-additional-properties#) 292 | (let [~e {:error :invalid-additional-properties 293 | :invalid-additional-properties invalid-additional-properties# 294 | :data ~data}] 295 | ~(error e)) 296 | ~(ok))) 297 | 298 | :default 299 | (ok))))))))) 300 | 301 | (defn validate-property-count [{min "minProperties" max "maxProperties" :as schema} data error ok _] 302 | (when (or min max) 303 | (let [e (gensym "E")] 304 | `(cond 305 | ~@(when-not (type-is? schema "object") 306 | [`(not (map? ~data)) 307 | (ok)]) 308 | 309 | ~@(when min 310 | [`(< (count ~data) ~min) 311 | `(let [~e {:error :too-few-properties 312 | :minimum-properties ~min 313 | :data ~data}] 314 | ~(error e))]) 315 | ~@(when max 316 | [`(> (count ~data) ~max) 317 | `(let [~e {:error :too-many-properties 318 | :maximum-properties ~max 319 | :data ~data}] 320 | ~(error e))]) 321 | 322 | :default 323 | ~(ok))))) 324 | 325 | (defn validate-enum-value 326 | [{enum "enum"} data error ok _] 327 | (when-let [allowed-values (and enum (into #{} enum))] 328 | (let [e (gensym "E")] 329 | `(if-not (~allowed-values ~data) 330 | (let [~e {:error :invalid-enum-value 331 | :data ~data 332 | :allowed-values ~allowed-values}] 333 | ~(error e)) 334 | ~(ok))))) 335 | 336 | (defn validate-array-items [{item-schema "items" 337 | additional-items "additionalItems" :as schema} data error ok options] 338 | (when item-schema 339 | (let [e (gensym "E") 340 | c (gensym "C") 341 | item (gensym "ITEM") 342 | item-error (gensym "ITEM-ERROR")] 343 | `(if-not (sequential? ~data) 344 | ~(ok) 345 | ~(cond 346 | ;; Schema is a map: validate all items against it 347 | (map? item-schema) 348 | `(loop [errors# [] 349 | i# 0 350 | [~item & items#] ~data] 351 | (if-not ~item 352 | (if (empty? errors#) 353 | ~(ok) 354 | (let [~e {:error :array-items 355 | :data ~data 356 | :items errors#}] 357 | ~(error e))) 358 | (let [item-error# 359 | ~(if (and (map? item-schema) (item-schema "enum")) 360 | (validate-enum-value item-schema item 361 | identity 362 | (constantly nil) 363 | options) 364 | (validate item-schema item options))] 365 | (recur (if item-error# 366 | (conj errors# (assoc item-error# 367 | :position i#)) 368 | errors#) 369 | (inc i#) 370 | items#)))) 371 | 372 | ;; Schema is an array, validate each index with its own schema 373 | (sequential? item-schema) 374 | `(let [~c (count ~data)] 375 | (if-let [~e (or 376 | #_(when (< (count ~data) ~(count item-schema)) 377 | {:error :too-few-items 378 | :expected-item-count ~(count item-schema) 379 | :actual-item-count (count ~data) 380 | :data ~data}) 381 | 382 | ~@(for [i (range (count item-schema))] 383 | `(when (> ~c ~i) 384 | (let [~item (nth ~data ~i)] 385 | ~(validate (nth item-schema i) item options)))) 386 | 387 | ;; If additional items is false, don't allow more items 388 | ~@(when (false? additional-items) 389 | [`(when (> ~c ~(count item-schema)) 390 | {:error :additional-items-not-allowed 391 | :additional-items (drop ~(count item-schema) ~data) 392 | :data ~data})]) 393 | 394 | ;; If additional items is a schema, check each against it 395 | ~@(when (map? additional-items) 396 | [`(some (fn [~item] 397 | (when-let [e# ~(validate additional-items item options)] 398 | {:error :additional-item-does-not-match-schema 399 | :item-error e# 400 | :data ~data})) 401 | (drop ~(count item-schema) ~data))]) 402 | )] 403 | ~(error e) 404 | ~(ok)))))))) 405 | 406 | (defn validate-array-item-count [{min-items "minItems" max-items "maxItems"} data error ok options] 407 | (when (or min-items max-items) 408 | (let [e (gensym "E") 409 | items (gensym "ITEMS")] 410 | `(if-not (sequential? ~data) 411 | ~(ok) 412 | (let [~items (count ~data)] 413 | (cond 414 | ~@(when min-items 415 | [`(> ~min-items ~items) 416 | `(let [~e {:error :wrong-number-of-elements 417 | :minimum ~min-items :actual ~items}] 418 | ~(error e))]) 419 | 420 | ~@(when max-items 421 | [`(< ~max-items ~items) 422 | `(let [~e {:error :wrong-number-of-elements 423 | :maximum ~max-items :actual ~items}] 424 | ~(error e))]) 425 | 426 | :default 427 | ~(ok))))))) 428 | 429 | (defn validate-array-unique-items [{unique-items "uniqueItems"} data error ok _] 430 | (when unique-items 431 | (let [item-count (gensym "IC") 432 | e (gensym "E") 433 | c (gensym "C")] 434 | `(if-not (sequential? ~data) 435 | ~(ok) 436 | (let [~c (count ~data)] 437 | (loop [seen# #{} 438 | duplicates# #{} 439 | i# 0] 440 | (if (= i# ~c) 441 | (if-not (empty? duplicates#) 442 | (let [~e {:error :duplicate-items-not-allowed 443 | :duplicates duplicates#}] 444 | ~(error e)) 445 | ~(ok)) 446 | (let [item# (nth ~data i#)] 447 | (recur (conj seen# item#) 448 | (if (seen# item#) 449 | (conj duplicates# item#) 450 | duplicates#) 451 | (inc i#)))))))))) 452 | 453 | (defn validate-not [{schema "not"} data error ok options] 454 | (when schema 455 | (let [e (gensym "E")] 456 | `(let [~e ~(validate schema data options)] 457 | (if (nil? ~e) 458 | (let [~e {:error :should-not-match 459 | :schema ~schema 460 | :data ~data}] 461 | ~(error e)) 462 | ~(ok)))))) 463 | 464 | (defn validate-all-of 465 | "Validate match against all of the given schemas." 466 | [{all-of "allOf"} data error ok options] 467 | (when-not (empty? all-of) 468 | (let [e (gensym "E")] 469 | `(if-not (and 470 | ~@(for [schema all-of] 471 | `(nil? ~(validate schema data options)))) 472 | (let [~e {:error :does-not-match-all-of 473 | :all-of ~all-of 474 | :data ~data}] 475 | ~(error e)) 476 | ~(ok))))) 477 | 478 | (defn validate-any-of 479 | "Validate match against any of the given schemas." 480 | [{any-of "anyOf"} data error ok options] 481 | (when-not (empty? any-of) 482 | (let [e (gensym "E")] 483 | `(let [errors# [~@(for [schema any-of] 484 | (validate schema data options))]] 485 | (if (some nil? errors#) 486 | ~(ok) 487 | (let [~e {:error :does-not-match-any-of 488 | :any-of ~any-of 489 | :data ~data 490 | :errors errors#}] 491 | ~(error e))))))) 492 | 493 | (defn validate-one-of 494 | "Validate match against one (and only one) of the given schemas." 495 | [{one-of "oneOf"} data error ok options] 496 | (when-not (empty? one-of) 497 | (let [e (gensym "E") 498 | c (gensym "C")] 499 | `(let [~c (+ ~@(for [s one-of] 500 | (validate s data (constantly 0) (constantly 1) options)))] 501 | (if-not (= 1 ~c) 502 | (let [~e {:error :item-does-not-match-exactly-one-schema 503 | :data ~data 504 | :matched-schemas ~c}] 505 | ~(error e)) 506 | ~(ok)))))) 507 | 508 | (defn validate-dependencies [{dependencies "dependencies" :as schema} data error ok options] 509 | (when-not (empty? dependencies) 510 | (let [e (gensym "E" )] 511 | `(cond 512 | ~@(when-not (type-is? schema "object") 513 | [`(not (map? ~data)) 514 | (ok)]) 515 | 516 | ~@(mapcat 517 | (fn [[property schema-or-properties]] 518 | [`(and (contains? ~data ~property) 519 | ~(if (map? schema-or-properties) 520 | (validate schema-or-properties data options) 521 | `(or ~@(for [p schema-or-properties] 522 | `(not (contains? ~data ~p)))))) 523 | `(let [~e {:error :dependency-mismatch 524 | :dependency {~property ~schema-or-properties} 525 | :data ~data}] 526 | ~(error e))]) 527 | dependencies) 528 | 529 | :default 530 | ~(ok))))) 531 | 532 | (def validations [#'validate-not #'validate-all-of #'validate-any-of #'validate-one-of 533 | #'validate-dependencies 534 | #'validate-type 535 | #'validate-enum-value 536 | #'validate-number-bounds 537 | #'validate-string-length #'validate-string-pattern validate-string-format 538 | #'validate-properties #'validate-property-count 539 | #'validate-array-items #'validate-array-item-count #'validate-array-unique-items]) 540 | 541 | (defn validate 542 | ([schema data options] 543 | (validate schema data identity (constantly nil) options)) 544 | ([schema data error ok options] 545 | (let [options (ref/root-schema options schema) 546 | schema (resolve-schema schema options) 547 | e (gensym "E") 548 | definitions (get schema "definitions")] 549 | `(or ~@(for [validate-fn validations 550 | :let [form (validate-fn schema data error ok options)] 551 | :when form] 552 | form))))) 553 | 554 | (defmacro make-validator 555 | "Create a validator function. The schema and options will be evaluated at compile time. 556 | 557 | An map of options can be given that supports the keys: 558 | :ref-resolver Function for loading referenced schemas. Takes in 559 | the schema URI and must return the schema parsed form. 560 | Default just tries to read it as a file via slurp and parse. 561 | 562 | :draft3-required when set to true, support draft3 style required (in property definition), 563 | defaults to false 564 | 565 | :lax-date-time-format? when set to true, allow more variation in date format, 566 | normally only strict RFC3339 dates are valid" 567 | ([schema options] 568 | (let [schema (eval schema) 569 | options (-> (eval options) 570 | (assoc :ref-resolver (or (:ref-resolver options) 571 | (memoize ref/resolve-ref)))) 572 | data (gensym "DATA")] 573 | `(fn [~data] 574 | ~(validate schema data options))))) 575 | -------------------------------------------------------------------------------- /src/webjure/json_schema/validator/string.clj: -------------------------------------------------------------------------------- 1 | (ns webjure.json-schema.validator.string) 2 | 3 | (defn length 4 | "Calculate string length in unicode codepoints" 5 | [str] 6 | (let [string-length (count str)] 7 | (loop [i 0 8 | len 0] 9 | (if (= i string-length) 10 | len 11 | (recur (+ i (Character/charCount (.codePointAt str i))) 12 | (inc len)))))) 13 | -------------------------------------------------------------------------------- /test/resources/address-and-phone-additional-properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": { 3 | "streetAddress": "21 2nd Street", 4 | "city": "New York" 5 | }, 6 | "phoneNumber": [ 7 | { 8 | "location": "home", 9 | "code": 44 10 | } 11 | ], 12 | "youDidntExpectMe": true, 13 | "orMe": 42 14 | } 15 | -------------------------------------------------------------------------------- /test/resources/address-and-phone-city-and-code-missing.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": { 3 | "streetAddress": "21 2nd Street" 4 | }, 5 | "phoneNumber": [ 6 | { 7 | "location": "home" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/resources/address-and-phone-null.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": { 3 | "streetAddress": "21 2nd Street", 4 | "city": null 5 | }, 6 | "phoneNumber": [ 7 | { 8 | "location": "home", 9 | "code": 44 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/resources/address-and-phone-valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": { 3 | "streetAddress": "21 2nd Street", 4 | "city": "New York" 5 | }, 6 | "phoneNumber": [ 7 | { 8 | "location": "home", 9 | "code": 44 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/resources/address-and-phone.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://jsonschema.net", 4 | "type": "object", 5 | "properties": { 6 | "address": { 7 | "id": "http://jsonschema.net/address", 8 | "type": "object", 9 | "properties": { 10 | "streetAddress": { 11 | "id": "http://jsonschema.net/address/streetAddress", 12 | "type": "string" 13 | }, 14 | "city": { 15 | "id": "http://jsonschema.net/address/city", 16 | "type": "string" 17 | } 18 | }, 19 | "required": [ 20 | "streetAddress", 21 | "city" 22 | ] 23 | }, 24 | "phoneNumber": { 25 | "id": "http://jsonschema.net/phoneNumber", 26 | "type": "array", 27 | "items": { 28 | "id": "http://jsonschema.net/phoneNumber/0", 29 | "type": "object", 30 | "properties": { 31 | "location": { 32 | "id": "http://jsonschema.net/phoneNumber/0/location", 33 | "type": "string" 34 | }, 35 | "code": { 36 | "id": "http://jsonschema.net/phoneNumber/0/code", 37 | "type": "integer" 38 | } 39 | } 40 | } 41 | } 42 | }, 43 | "required": [ 44 | "address", 45 | "phoneNumber" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /test/resources/format.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://jsonschema.net", 4 | "type": "object", 5 | "properties": { 6 | "host": { 7 | "type": ["string", "null"], 8 | "format": "hostname" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/resources/person-invalid.json: -------------------------------------------------------------------------------- 1 | {"name": "Rolf Teflon", 2 | "age": 33, 3 | "contact": { 4 | "address": { 5 | "streetAddress": "Some street 1", 6 | "city": "Localtown" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/resources/person-valid.json: -------------------------------------------------------------------------------- 1 | {"name": "Rolf Teflon", 2 | "age": 33, 3 | "contact": { 4 | "address": { 5 | "streetAddress": "Some street 1", 6 | "city": "Localtown" 7 | }, 8 | "phoneNumber": [ 9 | {"location": "work", "code": 555222}, 10 | {"location": "pager", "code": 666111} 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/resources/person.schema.json: -------------------------------------------------------------------------------- 1 | {"type": "object", 2 | "properties": {"name": {"type": "string"}, 3 | "age": {"type": "integer"}, 4 | "contact": {"$ref": "test/resources/address-and-phone.schema.json"}}} 5 | 6 | -------------------------------------------------------------------------------- /test/webjure/json_schema/suite_test.clj: -------------------------------------------------------------------------------- 1 | (ns webjure.json-schema.suite-test 2 | "Tests for https://github.com/json-schema-org/JSON-Schema-Test-Suite. 3 | The test suite is added as a submodule under test/resources." 4 | (:require [clojure.test :as t :refer [deftest is testing]] 5 | [webjure.json-schema.test-util :refer [validate-fn]] 6 | [clojure.java.io :as io] 7 | [cheshire.core :as cheshire] 8 | [clojure.string :as str] 9 | [webjure.json-schema.ref :as ref])) 10 | 11 | (defn ref-resolver [uri] 12 | (ref/resolve-ref (str/replace uri 13 | "http://localhost:1234" 14 | "file:test/resources/JSON-Schema-Test-Suite/remotes/"))) 15 | 16 | (def exclusions 17 | {;; Skip invalid definition in the schema 18 | "definitions" {:skip true} ;; {"invalid definition" {:macro false :fn false}} 19 | 20 | ;; Skip recursive schema in the macro version as it 21 | ;; creates an ever expanding function (causing a stack overflow) 22 | "ref" {"root pointer ref" {:macro false} 23 | "remote ref, containing refs itself" {:macro false} 24 | "Recursive references between schemas" {:skip true} 25 | :options {:ref-resolver ref-resolver}} 26 | 27 | ;; We don't have a regex for a regex 28 | "ecmascript-regex" {:skip true} 29 | 30 | ;; Cheat (a little bit) here, change http URLs to file URIs so that 31 | ;; we don't need to start an HTTP server. Clojure slurp works for 32 | ;; both URI types the same. 33 | "refRemote" {"base URI change - change folder" {:skip true} 34 | "base URI change - change folder in subschema" {:skip true} 35 | "root ref in remote ref" {:skip true} 36 | :options {:ref-resolver ref-resolver}} 37 | 38 | "format" {"validation of date-time strings" {"a valid date-time string without second fraction" {:skip true}}} 39 | }) 40 | 41 | (def suite-tests 42 | (->> (io/file "test/resources/JSON-Schema-Test-Suite/tests/draft4") 43 | file-seq 44 | (filter #(.endsWith (.getName %) ".json")) 45 | (mapv (juxt #(let [n (.getName %)] 46 | (subs n 0 (- (count n) 5))) 47 | #(cheshire/parse-string (slurp %)))) 48 | (into {}))) 49 | 50 | (defn sym [name] 51 | (symbol (str/replace name " " "-"))) 52 | 53 | (defmacro define-suite-tests [] 54 | (let [schema-sym (gensym "SCHEMA")] 55 | `(do 56 | ~@(for [[test-name tests] suite-tests 57 | :let [options (or (get-in exclusions [test-name :options]) {})] 58 | :when (not (:skip (exclusions test-name)))] 59 | `(deftest ~(sym (str "suite-" test-name)) 60 | ~@(for [{desc "description" 61 | schema "schema" 62 | tests "tests"} tests 63 | :let [schema (ref/initialize-id-path schema) 64 | options (merge options (get-in exclusions [test-name desc :options]))] 65 | :when (not (:skip (get-in exclusions [test-name desc])))] 66 | `(testing ~desc 67 | (let [~schema-sym (validate-fn ~schema ~options ~(get-in exclusions [test-name desc]))] 68 | ~@(for [{test-desc "description" 69 | data "data" 70 | valid? "valid"} tests 71 | :when (not (:skip (get-in exclusions [test-name desc test-desc])))] 72 | (if valid? 73 | `(is (nil? (~schema-sym ~data)) 74 | ~(str "Test '" test-desc "' should be valid")) 75 | `(is (~schema-sym ~data) 76 | ~(str "Test '" test-desc "' should NOT be valid")))))))))))) 77 | 78 | (define-suite-tests) 79 | -------------------------------------------------------------------------------- /test/webjure/json_schema/test_util.clj: -------------------------------------------------------------------------------- 1 | (ns webjure.json-schema.test-util 2 | (:require [clojure.test :as t :refer [is]] 3 | [webjure.json-schema.validator :refer [validate]] 4 | [webjure.json-schema.validator.macro :refer [make-validator]] 5 | [cheshire.core :as cheshire])) 6 | 7 | (defn p [resource-path] 8 | (->> resource-path 9 | (str "test/resources/") 10 | slurp cheshire/parse-string)) 11 | 12 | ;; Define a macro that defines a new validator function. 13 | ;; The function calls both the function style validation and 14 | ;; the generated macro version and verifies that they validate 15 | ;; errors in the same way. 16 | 17 | (defmacro validate-fn [schema & opts] 18 | (let [schema (if (string? schema) 19 | (p schema) 20 | schema) 21 | validation-opts (or (first opts) {}) 22 | test-opts (merge {:macro true :fn true} 23 | (second opts)) 24 | data (gensym "DATA") 25 | macro-validator (gensym "MACRO-VALIDATOR") 26 | fn-res (gensym "FN-RES") 27 | macro-res (gensym "MACRO-RES")] 28 | ;;(println "EXPAND TEST VALIDATOR FOR " (pr-str schema) " TEST-OPTS: " (pr-str test-opts)) 29 | `(let [~macro-validator ~(when (:macro test-opts) 30 | `(make-validator ~schema ~validation-opts))] 31 | (fn [~data] 32 | (let [~fn-res ~(when (:fn test-opts) 33 | `(validate ~schema ~data ~validation-opts)) 34 | ~macro-res ~(when (:macro test-opts) 35 | `(~macro-validator ~data))] 36 | ~(when (and (:macro test-opts) (:fn test-opts)) 37 | `(is (= ~fn-res ~macro-res) 38 | "function and macro versions validate in the same way")) 39 | ~(cond 40 | (:fn test-opts) 41 | fn-res 42 | 43 | (:macro test-opts) 44 | macro-res)))))) 45 | 46 | (defmacro defvalidate [name schema & opts] 47 | (let [schema (if (string? schema) 48 | (p schema) 49 | schema) 50 | opts (or (first opts) {})] 51 | `(defn ~name [data#] 52 | (let [fn-res# (validate ~schema data# ~opts) 53 | macro-res# ((make-validator ~schema ~opts) data#)] 54 | (is (= fn-res# macro-res#) 55 | "function and macro versions validate in the same way") 56 | fn-res#)))) 57 | -------------------------------------------------------------------------------- /test/webjure/json_schema/validator_test.clj: -------------------------------------------------------------------------------- 1 | (ns webjure.json-schema.validator-test 2 | (:require [cheshire.core :as cheshire] 3 | [clojure.test :refer [deftest is testing]] 4 | [webjure.json-schema.test-util :refer [defvalidate p]])) 5 | 6 | (defvalidate address-and-phone "address-and-phone.schema.json") 7 | 8 | (deftest validate-address-and-phone 9 | (testing "jsonschema.net example schema" 10 | (testing "valid json returns nil errors" 11 | (is (nil? (address-and-phone (p "address-and-phone-valid.json"))))) 12 | 13 | (testing "missing property error is reported" 14 | (let [e (address-and-phone (p "address-and-phone-city-and-code-missing.json"))] 15 | (is (= :properties (:error e))) 16 | ;; "city" is reported as missing because it is required 17 | (is (= :missing-property (get-in e [:properties "address" :properties "city" :error]))) 18 | 19 | ;; no errors in "phoneNumber" because missing "code" in first item is not required 20 | (is (nil? (get-in e [:properties "phoneNumber"]))))) 21 | 22 | (testing "additional properties are ok" 23 | (is (nil? (address-and-phone (p "address-and-phone-additional-properties.json"))))) 24 | 25 | (testing "null required value is validated" 26 | (is (= {:expected :string :error :wrong-type :data nil} 27 | (get-in (address-and-phone (p "address-and-phone-null.json")) 28 | [:properties "address" :properties "city"])))))) 29 | 30 | (defvalidate ref-schema "person.schema.json") 31 | (deftest validate-referenced-schema 32 | (testing "person schema that links to address and phone schema" 33 | (let [s (p "person.schema.json")] 34 | (testing "valid json returns nil errors" 35 | (is (nil? (ref-schema (p "person-valid.json"))))) 36 | (testing "linked schema errors are reported" 37 | (is (= :missing-property 38 | (get-in (ref-schema (p "person-invalid.json")) 39 | [:properties "contact" :properties "phoneNumber" :error]))))))) 40 | 41 | (defvalidate valid-enum {"type" "string" "enum" ["foo" "bar"]}) 42 | (deftest validate-enum 43 | (testing "enum values are checked" 44 | (is (nil? (valid-enum "foo"))) 45 | (is (= {:error :invalid-enum-value 46 | :data "xuxu" 47 | :allowed-values #{"foo" "bar"}} 48 | (valid-enum "xuxu"))))) 49 | 50 | 51 | (defvalidate bool {"type" "boolean"}) 52 | (deftest validate-boolean 53 | (testing "boolean values are checked" 54 | (is (nil? (bool true))) 55 | (is (nil? (bool false))) 56 | (is (every? #(= :wrong-type (:error %)) 57 | (map #(bool %) 58 | ["foo" 42 [4 5] {"name" "Test"}]))))) 59 | 60 | (defvalidate validate-integer {"type" "integer" "minimum" 1 "maximum" 10}) 61 | (defvalidate validate-integer-excl {"type" "integer" 62 | "minimum" 1 63 | "maximum" 10 64 | "exclusiveMinimum" true 65 | "exclusiveMaximum" true}) 66 | 67 | (deftest validate-integer-bounds 68 | (testing "integer bounds are checked" 69 | (testing "non exclusive bounds work" 70 | (testing "valid values return nil" 71 | (is (every? nil? 72 | (map #(validate-integer %) 73 | (range 1 1))))) 74 | 75 | (testing "too low values report error with minimum" 76 | (is (= {:error :out-of-bounds :data 0 :minimum 1 :exclusive false} 77 | (validate-integer 0)))) 78 | 79 | (testing "too high values report error with maximum" 80 | (is (= {:error :out-of-bounds :data 11 :maximum 10 :exclusive false} 81 | (validate-integer 11))))) 82 | (testing "exclusive bounds works" 83 | (is (nil? (validate-integer-excl 2))) 84 | (is (nil? (validate-integer-excl 9))) 85 | (is (= {:error :out-of-bounds :data 1 :minimum 1 :exclusive true} 86 | (validate-integer-excl 1))) 87 | (is (= {:error :out-of-bounds :data 10 :maximum 10 :exclusive true} 88 | (validate-integer-excl 10)))))) 89 | 90 | (defvalidate draft3-requires {"type" "object" 91 | "properties" {"name" {"type" "string" 92 | "required" true} 93 | "age" {"type" "integer"}}} 94 | {:draft3-required true}) 95 | 96 | (deftest validate-draft3-requires 97 | (is (nil? (draft3-requires (cheshire/parse-string "{\"name\": \"Test\"}") ))) 98 | (is (= {:error :properties 99 | :data {"age" 42} 100 | :properties {"name" {:error :missing-property}}} 101 | (draft3-requires (cheshire/parse-string "{\"age\": 42}"))))) 102 | 103 | (defvalidate numb {"type" "number"}) 104 | (deftest validate-number 105 | (is (nil? (numb 3.33))) 106 | (is (= {:error :wrong-type 107 | :expected :number 108 | :data "foo"} 109 | (numb "foo")))) 110 | 111 | (defvalidate valid-array {"type" "object" 112 | "properties" {"things" {"type" "array" 113 | "items" {"type" "string"} 114 | "minItems" 3 115 | "maxItems" 4 116 | "uniqueItems" true}}}) 117 | 118 | (deftest validate-minimum-number-of-items 119 | (let [json (cheshire/parse-string "{\"things\" : [\"value\", \"value\"] }") 120 | errors (valid-array json) 121 | expected-errors {:error :properties 122 | :data {"things" ["value" "value"]} 123 | :properties {"things" {:error :wrong-number-of-elements 124 | :minimum 3 125 | :actual 2}}}] 126 | (is (= expected-errors errors)))) 127 | 128 | (deftest validate-maximum-number-of-items 129 | (let [json (cheshire/parse-string "{\"things\" : [\"value\", \"value\", \"value\", \"value\", \"value\"] }") 130 | errors (valid-array json) 131 | expected-errors {:error :properties 132 | :data {"things" ["value" "value" "value" "value" "value"]} 133 | :properties {"things" {:error :wrong-number-of-elements 134 | :maximum 4 135 | :actual 5}}}] 136 | (is (= expected-errors errors)))) 137 | 138 | (deftest validate-unique-items 139 | (let [json (cheshire/parse-string "{\"things\" : [\"value\", \"value\", \"value\", \"value\"] }") 140 | errors (valid-array json) 141 | expected-errors {:error :properties, 142 | :data {"things" ["value" "value" "value" "value"]} 143 | :properties {"things" {:error :duplicate-items-not-allowed 144 | :duplicates #{"value"}}}}] 145 | (is (= expected-errors errors)))) 146 | 147 | (deftest validate-valid-array 148 | (let [json (cheshire/parse-string "{\"things\" : [\"first\", \"second\", \"third\"] }") 149 | errors (valid-array json)] 150 | (is (nil? errors)))) 151 | 152 | (defvalidate enum-array {"type" "array" 153 | "items" {"enum" ["foo" "bar"]}}) 154 | 155 | (deftest validate-enum-array 156 | (let [json (cheshire/parse-string "[\"foo\", \"kek\"]") 157 | errors (enum-array json)] 158 | (is (= errors {:error :array-items 159 | :data ["foo" "kek"] 160 | :items [{:error :invalid-enum-value 161 | :data "kek" 162 | :allowed-values #{"foo" "bar"} 163 | :position 1}]})))) 164 | 165 | (deftest validate-enum-array-ok 166 | (let [json (cheshire/parse-string "[\"foo\", \"bar\"]") 167 | errors (enum-array json)] 168 | (is (nil? errors)))) 169 | 170 | (defvalidate valid-date {"type" "object" 171 | "properties" {"date" {"id" "http://jsonschema.net/date" 172 | "type" "string" 173 | "format" "date-time"}}} 174 | {:lax-date-time-format? true}) 175 | 176 | (deftest validate-valid-date 177 | (let [json (cheshire/parse-string "{\"date\": \"2015-01-30T12:00:00Z\"}") 178 | errors (valid-date json)] 179 | (is (nil? errors)))) 180 | 181 | (deftest validate-invalid-date 182 | (let [json (cheshire/parse-string "{\"date\": \"foo\"}") 183 | errors (valid-date json) 184 | expected-errors {:data {"date" "foo"} 185 | :error :properties 186 | :properties {"date" {:data "foo" 187 | :error :wrong-format 188 | :expected :date-time}}}] 189 | (is (= expected-errors errors)))) 190 | 191 | (defvalidate multiple-of-8 {"type" "number" 192 | "multipleOf" 8}) 193 | 194 | (deftest validate-multiple-of-8 195 | (is (nil? (multiple-of-8 16))) 196 | (is (nil? (multiple-of-8 256))) 197 | (is (multiple-of-8 55))) 198 | 199 | ;; Definition example from validation spec 200 | (defvalidate definitions (cheshire/parse-string "{ 201 | \"type\": \"array\", 202 | \"items\": { \"$ref\": \"#/definitions/positiveInteger\" }, 203 | \"definitions\": { 204 | \"positiveInteger\": { 205 | \"type\": \"integer\", 206 | \"minimum\": 0, 207 | \"exclusiveMinimum\": true 208 | } 209 | } 210 | }")) 211 | 212 | (deftest validate-definitions 213 | (is (nil? (definitions [1 2 3]))) 214 | (is (= {:error :array-items 215 | :items [{:exclusive true :minimum 0 216 | :error :out-of-bounds 217 | :data -2 218 | :position 1}] 219 | :data [1 -2 3]} 220 | (definitions [1 -2 3])))) 221 | 222 | (defvalidate format-or-null "format.schema.json") 223 | (deftest validate-format-not-checked-for-null 224 | (is (nil? (format-or-null {"host" nil}))) 225 | (is (nil? (format-or-null {"host" "example.com"}))) 226 | (is (= {:error :wrong-format 227 | :expected :hostname 228 | :data "example.com 666"} 229 | (get-in (format-or-null {"host" "example.com 666"}) [:properties "host"])))) 230 | --------------------------------------------------------------------------------