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 | PARTIAL | OpenAPI Extension; implemented for oneOf schemas only |
JSON enum | DONE | |
JSON exclusiveMaximium | DONE | |
JSON exclusiveMinimum | 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 char s) |
JSON maxProperties | DONE | |
JSON maximum | DONE | |
JSON minItems | DONE | |
JSON minLength | DONE | Counts Unicode codepoints (not Java char s) |
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:
const
unique
enum
type
multipleOf
Java / 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
.
discriminator
OpenAPI JSON Schema extension. This is
only implemented when combined with oneOf
since a discriminator
makes no sense in combination with allOf
or someOf
.: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 maxContains
maxContains
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? These fine people already did:
Joost Diepenmaat & Remco van 't VeerEdit on sourcehut
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close