A pure-clojure library for validating ring requests & responses against OpenAPI v3 specifications.
Long term goal: Should work on any Clojure implementation.
Status: the core validator code has no dependencies and should work with minimal changes on any Clojure implementation. Clojure JVM and Babashka are currently tested as part of development.
| Target | Status | Remarks | 
|---|---|---|
| JVM Clojure | DONE | |
| Babashka | DONE | |
| ClojureScript | TODO | Patches welcome | 
| .NET Clojure | N/A | Not planned, patches welcome | 
Target: Complete implementation of Ring request/response maps against OpenAPI v3.1.0 (the current version of the OpenAPI specification). We do not intent to implement v2 or earlier OpenAPI specifications.
Status: mostly complete and usable for real world scenarios. See below table.
| Validation | Status | Remarks | 
|---|---|---|
| JSON $dynamicAnchor | N/A | Not implemented, not planned for development | 
| JSON $dynamicRef | N/A | Not implemented, not planned for development | 
| JSON $id | N/A | Not implemented, not planned for development | 
| JSON $ref | PARTIAL | Simple fragment identifiers only | 
| JSON $vocabulary | N/A | Not implemented, not planned for development | 
| JSON additionalProperties | DONE | |
| JSON allOf | DONE | Issues of sub-schemas are concatenated | 
| JSON anyOf | DONE | Reports :sub-issues | 
| JSON const | DONE | Using JS-style semantics (unified numeric types) | 
| JSON contains | DONE | Using JS-style semantics (unified numeric types) | 
| JSON contentEncoding | TODO | Should be configurable | 
| JSON contentMediaType | TODO | Should be configurable | 
| JSON contentSchema | TODO | |
| JSON dependentRequired | DONE | |
| JSON dependentSchemas | TODO | |
| JSON discriminator | TODO | OpenAPI Extension; never required, but should improve validation performance | 
| JSON enum | DONE | |
| JSON format | PARTIAL | Only validates "uuid", disabled by default | 
| JSON if then else | DONE | |
| JSON items | DONE | |
| JSON maxItems | DONE | |
| JSON maxLength | DONE | Counts Unicode codepoints (not Java chars) | 
| JSON maxProperties | DONE | |
| JSON maximum | DONE | |
| JSON minItems | DONE | |
| JSON minLength | DONE | Counts Unicode codepoints (not Java chars) | 
| JSON minProperties | DONE | |
| JSON minimum | DONE | |
| JSON multipleOf | DONE | |
| JSON not | DONE | |
| JSON oneOf | DONE | Reports :sub-issues | 
| JSON pattern | DONE | Using java.util.regex.Pattern | 
| JSON patternProperties | DONE | Using java.util.regex.Pattern | 
| JSON prefixItems | DONE | |
| JSON propertyNames | DONE | |
| JSON properties | DONE | |
| JSON required | DONE | |
| JSON type | DONE | Using JS-Style semantics (unified numeric types) | 
| JSON unevaluatedItems | TODO | |
| JSON unevaluatedProperties | TODO | |
| JSON uniqueItems | DONE | Using JS-style semantics (unified numeric types) | 
| JSON xml | TODO | OpenAPI Extension | 
| OpenAPI | PARTIAL | v3.1.0 mostly implemented | 
| OpenAPI $ref | PARTIAL | Simple fragment identifiers only | 
| OpenAPI Callback | N/A | Not relevant | 
| OpenAPI Components | DONE | Can be referred to using $ref | 
| OpenAPI Contact | N/A | Not relevant | 
| OpenAPI Encoding | TODO | Not implemented | 
| OpenAPI Header | DONE | |
| OpenAPI Info | N/A | Not relevant | 
| OpenAPI Licence | N/A | Not relevant | 
| OpenAPI Link | N/A | Not relevant | 
| OpenAPI Media Type | DONE | Any content-type, body must be parsed before validation | 
| OpenAPI OAuth Flow | TODO | |
| OpenAPI OAuth Flows | TODO | |
| OpenAPI Operation | DONE | Including headers and parameters | 
| OpenAPI Parameter | PARTIAL | Except allowEmptyValue,style=form+explode=true,deepObject=true | 
| OpenAPI Path Item | PARTIAL | Parameters in paths item are not validated | 
| OpenAPI Paths | DONE | |
| OpenAPI Request Body | DONE | |
| OpenAPI Response | DONE | |
| OpenAPI Responses | DONE | |
| OpenAPI Schema | PARTIAL | See JSON entries in this table for status per JSON Schema keyword | 
| OpenAPI Security Requirement | TODO | |
| OpenAPI Security Scheme | TODO | |
| OpenAPI Server | TODO | Not sure if relevant to validation | 
| OpenAPI Server Variable | N/A | Not relevant | 
| OpenAPI Tag | N/A | Not relevant | 
Validating OpenAPI requests/responses:
(require '[nl.jomco.openapi.v3.validator :as validator])
(require '[clojure.data.json :as json])
(require '[clojure.java.io :as io])
(def ooapi-spec
  (json/read (io/reader (io/file "ooapiv5.json")) {:key-fn identity))
(def validate
  (-> ooapi-spec
      (validator/validator-context nil)
      validator/interaction-validator))
(validate {:request  {:method       :get
                      :uri          "/courses"
                      :query-params {"pageNumber" "foo"
                                     "sort"       "something,-name"}}
           :response {:status  200
                      :headers {"content-type" "application/json; charset=utf8"}
                      :body    {"pagesize"        0
                                "pageNumber"      1
                                "hasPreviousPage" false
                                "hasNextPage"     "true"
                                "items"           []}}}
          [])
;; =>
[{:canonical-schema-path ["components" "parameters" "pageNumber"],
  :instance              "foo",
  :issue                 "coercion-error",
  :path                  [:request :query-params "pageNumber"],
  :schema                {"type" "integer"},
  :schema-path           ["paths" "/courses" "get" "parameters" 2 "schema"]}
 {:instance              "something",
  :path                  [:request :query-params "sort" 0],
  :schema-path           ["paths" "/courses" "get" "parameters" 8 "schema" "items" "enum"],
  :canonical-schema-path ["paths" "/courses" "get" "parameters" 8 "schema" "items" "enum"],
  :schema-keyword        "enum",
  :schema                {"enum" ["courseId" "name" "-courseId" "-name"]},
  :issue                 "schema-validation-error"}
 {:instance              {"pagesize"        0,
                          "pageNumber"      1,
                          "hasPreviousPage" false,
                          "hasNextPage"     "true",
                          "items"           []},
  :path                  [:response :body],
  :schema-path           ["paths" "/courses" "get" "responses" "200" "content"
                          "application/json" "schema" "allOf" 0 "required"],
  :canonical-schema-path ["components" "schemas" "Pagination" "required"],
  :hints                 {:missing ["pageSize"]},
  :schema-keyword        "required",
  :schema                {"required" ["pageSize" "pageNumber" "hasPreviousPage"
                                      "hasNextPage" "items"]},
  :issue                 "schema-validation-error"}
 {:instance              "true",
  :path                  [:response :body "hasNextPage"],
  :schema-path           ["paths" "/courses" "get" "responses" "200" "content"
                          "application/json" "schema" "allOf" 0 "properties" "hasNextPage" "type"],
  :canonical-schema-path ["components" "schemas" "Pagination" "properties" "hasNextPage" "type"],
  :schema-keyword        "type",
  :schema                {"type" "boolean"},
  :issue                 "schema-validation-error"}]
Validating JSON Schemas:
(require '[nl.jomco.openapi.v3.schema-validator :as validator])
(def validate
   (validator/schema-validator
      (validator/validator-context specification)
      ["path" "to" "schema" "in" "spec"]))
(def issues
  (validate instance ["path" "of" "instance"]))
Validations result in nil when no issues are present, or a
collection of issues.
Issues are maps with a key :issue with one of the following values:
"schema-validation-error" - instance (body or parameter part) did
not validate according to the JSON Schema specification."coercion-error" - can't coerce parameter to the correct type"method-error" - the request method did not match the specification"uri-error" - the uri path did not match the specification"content-type-error" - the request/response content type did not
match the specification."status-error" - the response status code did not match the
specification.An issue can optionally have one or more of the following keys:
:instance - the part of the document that failed to validate.:path - the absolute path to instance:schema-path the path in the specification that resolved to the
failing validation.:canonical-schema-path the absolute path to the schema of the
failing validation.:schema the relevant parts of the schema that did not
validate. Usually a map.:hints a map with additional information depending on the error.:sub-issues - for combining schemas (oneOf and anyOf), the
collections of results of the sub schema validations.:schema-keyword - for JSON Schema keyword validations, the "main"
schema keyword of the failing validation.Paths are vectors of keys (strings, keywords and integers).
Requests and resposes follow ring format:
:uri is the path of the request:request-params is a map with string keys:headers is a map of with string keys:method is a keyword :get, :put, :post etc:body if present should be a parsed document (probably JSON) and
have string keys.Interactions are maps with :request and :response keys
In JSON Schema (according to the JSON Schema Test Suite), numeric semantics are different from Java / Clojure:
Implications:
[5.0] is equal to [5][5, 5.0] does not have unique entries{"a": 5} is equal to {"a": 5.0}5.0 validates according to {"type": "integer"}Further more, some of the JSON Schema Test Suite expects exact results for operations (multipleOf) on decimal numbers (BigDecimal semantics).
Affected JSON Schemas keywords:
constuniqueenumtypemultipleOfJava / Clojure JSON parsers tend to parse 5.0 as a double, and
5 as a long, and you can't rely on a {"type": "integer"} schema
validation to restrict input -- 5.0 is explictly valid for
{"type": "integer"}.
Note: clojure.data.json has a :bigdec option, but that only
applies to numbers with decimal parts. Non-decimal numbers are
always parsed as long or BigInteger.
The JSON Schema validator,
nl.jomco.openapi.v3.validator.json-schema-validator by default
uses the semantics of JSON Schema Test Suite, while working with
standard JSON parsers.
The solution implemented is to use a json-coerce function
to coerce numbers (as scalars and in collections) to BigDecimals, and
then compare using the coerced values.
You can turn this off and use standard Clojure semantics by passing the
:numeric-coercion identity option to validator/validator-context.
:numeric-coercion
option.if then else JSON Schema keywordsprefixItems JSON Schema keywordpatternProperties and additionalProperties JSON Schema keywordsexclusiveMaximium and exclusiveMinimum keywordspropertyNames JSON Schema keywordminContains = 0 with maxContainsmaxContains without minContains$ref as part of JSON Schema# as root JSON pointerminLength and maxLength to count codepoints"const": null validationformat validations by default (according to spec)JSON-Schema-Test-Suite testsCopyright © 2022-2023 Joost Diepenmaat, Jomco BV
This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0.
This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version, with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html.
Can you improve this documentation?Edit on sourcehut
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs | 
| ← | Move to previous article | 
| → | Move to next article | 
| Ctrl+/ | Jump to the search field |