Liking cljdoc? Tell your friends :D

OpenAPI V3 Validator

A pure-clojure library for validating ring requests & responses against OpenAPI v3 specifications.

Build status

builds.sr.ht status

Deps

Clojars Project

Goals

Portable clojure

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.

TargetStatusRemarks
JVM ClojureDONE
BabashkaDONE
ClojureScriptTODOPatches welcome
.NET ClojureN/ANot planned, patches welcome

Schema validation

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.

ValidationStatusRemarks
JSON $dynamicAnchorN/ANot implemented, not planned for development
JSON $dynamicRefN/ANot implemented, not planned for development
JSON $idN/ANot implemented, not planned for development
JSON $refPARTIALSimple fragment identifiers only
JSON $vocabularyN/ANot implemented, not planned for development
JSON additionalPropertiesDONE
JSON allOfDONEIssues of sub-schemas are concatenated
JSON anyOfDONEReports :sub-issues
JSON constDONEUsing JS-style semantics (unified numeric types)
JSON containsDONEUsing JS-style semantics (unified numeric types)
JSON contentEncodingTODOShould be configurable
JSON contentMediaTypeTODOShould be configurable
JSON contentSchemaTODO
JSON dependentRequiredDONE
JSON dependentSchemasTODO
JSON discriminatorPARTIALOpenAPI Extension; implemented for oneOf schemas only
JSON enumDONE
JSON formatPARTIALOnly validates "uuid", disabled by default
JSON if then elseDONE
JSON itemsDONE
JSON maxItemsDONE
JSON maxLengthDONECounts Unicode codepoints (not Java chars)
JSON maxPropertiesDONE
JSON maximumDONE
JSON minItemsDONE
JSON minLengthDONECounts Unicode codepoints (not Java chars)
JSON minPropertiesDONE
JSON minimumDONE
JSON multipleOfDONE
JSON notDONE
JSON oneOfDONEReports :sub-issues
JSON patternDONEUsing java.util.regex.Pattern
JSON patternPropertiesDONEUsing java.util.regex.Pattern
JSON prefixItemsDONE
JSON propertyNamesDONE
JSON propertiesDONE
JSON requiredDONE
JSON typeDONEUsing JS-Style semantics (unified numeric types)
JSON unevaluatedItemsTODO
JSON unevaluatedPropertiesTODO
JSON uniqueItemsDONEUsing JS-style semantics (unified numeric types)
JSON xmlTODOOpenAPI Extension
OpenAPIPARTIALv3.1.0 mostly implemented
OpenAPI $refPARTIALSimple fragment identifiers only
OpenAPI CallbackN/ANot relevant
OpenAPI ComponentsDONECan be referred to using $ref
OpenAPI ContactN/ANot relevant
OpenAPI EncodingTODONot implemented
OpenAPI HeaderDONE
OpenAPI InfoN/ANot relevant
OpenAPI LicenceN/ANot relevant
OpenAPI LinkN/ANot relevant
OpenAPI Media TypeDONEAny content-type, body must be parsed before validation
OpenAPI OAuth FlowTODO
OpenAPI OAuth FlowsTODO
OpenAPI OperationDONEIncluding headers and parameters
OpenAPI ParameterPARTIALExcept allowEmptyValue, style=form + explode=true, deepObject=true
OpenAPI Path ItemPARTIALParameters in paths item are not validated
OpenAPI PathsDONE
OpenAPI Request BodyDONE
OpenAPI ResponseDONE
OpenAPI ResponsesDONE
OpenAPI SchemaPARTIALSee JSON entries in this table for status per JSON Schema keyword
OpenAPI Security RequirementTODO
OpenAPI Security SchemeTODO
OpenAPI ServerTODONot sure if relevant to validation
OpenAPI Server VariableN/ANot relevant
OpenAPI TagN/ANot relevant

Other TODO items

Usage

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"]))

Validation issue format

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).

Interaction / request / response format

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

Equality semantics

In JSON Schema (according to the JSON Schema Test Suite), numeric semantics are different from Java / Clojure:

  • There is only one Number type for decimals and integers, so 10.0 (decimal) is equal to 10 (integer).
  • No complex numbers

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.

CHANGELOG

0.2.3

  • Implement 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.

0.2.2

  • Fix test dependencies on babashka; use org.babashka/json

0.2.1

  • Pluggable numeric comparison semantics using :numeric-coercion option.
  • Fix "uri-error" and "method-error" when request does not match specification

0.2.0

  • Implement if then else JSON Schema keywords
  • Implement prefixItems JSON Schema keyword
  • Implement patternProperties and additionalProperties JSON Schema keywords
  • Fix exclusiveMaximium and exclusiveMinimum keywords
  • Implement propertyNames JSON Schema keyword
  • Fix minContains = 0 with maxContains
  • Fix maxContains without minContains
  • Handle $ref as part of JSON Schema
  • Handle # as root JSON pointer
  • Handle trailing "/" in JSON Pointers
  • Fix minLength and maxLength to count codepoints
  • Fix "const": null validation
  • Disabled format validations by default (according to spec)
  • Add JSON-Schema-Test-Suite tests
  • Include license info in pom.xml

0.1.83

  • Removed txt files that broke cljdoc.org

0.1.82

  • Many small fixes and JSON schema keywords supported
  • Documentation of what is supported
  • Documentation of issue format
  • OpenAPI specification docs and request/reponse bodies should now be parsed /without/ keywordized keys (plain strings).

0.1.77

  • Moved to deps.edn / tools.build project structure
  • Renamed namespaces
  • Lots of little fixes

0.1.0-SNAPSHOT

  • Initial test release

License

Copyright © 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 is a website building & hosting documentation for Clojure/Script libraries

× close