Repository: oliyh/martian
Pull request #92 (2020-08-18 16:14:47)

Enabling bootstrapping from remote openapi schema #91 ...
- autodetecting openapi/swagger
- openapi ns now uses keywords
- derive supported content-types from interceptors


Patch:
diff --git a/README.md b/README.md
index e04fce5..a86bbb8 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 Calling HTTP endpoints can be complicated. You have to construct the right URL with the right route parameters, remember
 what the query parameters are, what method to use, how to encode the body and many other things that leak into your codebase.
 
-**Martian** takes a description of these details (either from your [Swagger](http://swagger.io/) server,
+**Martian** takes a description of these details (either from your [OpenAPI/Swagger](http://swagger.io/) server,
 or just as lovely Clojure data) and provides a client interface to the API that abstracts you away from HTTP and lets you
 simply call operations with parameters, keeping your codebase clean.
 
@@ -11,7 +11,7 @@ You can bootstrap it in one line and start calling the server:
 (require '[martian.core :as martian]
          '[martian.clj-http :as martian-http])
 
-(let [m (martian-http/bootstrap-swagger "https://pedestal-api.herokuapp.com/swagger.json")]
+(let [m (martian-http/bootstrap-openapi "https://pedestal-api.herokuapp.com/swagger.json")]
   (martian/response-for m :create-pet {:name "Doggy McDogFace" :type "Dog" :age 3})
   ;; => {:status 201 :body {:id 123}}
 
@@ -24,7 +24,7 @@ but any other HTTP library can be used due to the extensibility of Martian's int
 It also allows custom behaviour to be injected in a uniform and powerful way.
 
 The `martian-test` library allows you to assert that your code constructs valid requests to remote servers without ever
-actually calling them, using the Swagger spec to validate the parameters. It can also generate responses in the same way,
+actually calling them, using the OpenApi spec to validate the parameters. It can also generate responses in the same way,
 ensuring that your response handling code is also correct. Examples are below.
 
 `martian-re-frame` integrates martian event handlers into `re-frame`, simplifying connecting your UI to data sources.
@@ -45,7 +45,7 @@ ensuring that your response handling code is also correct. Examples are below.
 [![Clojars Project](https://img.shields.io/clojars/v/martian-re-frame.svg)](https://clojars.org/martian-re-frame) [![cljdoc badge](https://cljdoc.org/badge/martian-re-frame/martian-re-frame)](https://cljdoc.org/d/martian-re-frame/martian-re-frame/CURRENT)
 
 ## Features
-- Bootstrap an instance from just a Swagger url or provide your own API mapping
+- Bootstrap an instance from just a OpenAPI/Swagger url or provide your own API mapping
 - Modular with support for `clj-http` and `httpkit` (Clojure) and `cljs-http` (ClojureScript)
 - Build urls and request maps from code or generate and perform the request, returning the response
 - Explore an API from your REPL
@@ -53,7 +53,7 @@ ensuring that your response handling code is also correct. Examples are below.
 - Negotiates the most efficient content-type and handles serialisation and deserialisation including `transit`, `edn` and `json`
 - Easy to add support for any other content-type
 - Support for integration testing without requiring external HTTP stubs
-- Routes are named as idiomatic kebab-case keywords of the `operationId` of the endpoint in the Swagger definition
+- Routes are named as idiomatic kebab-case keywords of the `operationId` of the endpoint in the OpenAPI/Swagger definition
 - Parameters are aliased to kebab-case keywords so that your code remains idiomatic, neat and clean
 - Simple, data driven behaviour with low coupling using libraries and patterns you already know
 - Pure client code, no server code or modifications required
@@ -62,15 +62,15 @@ For more details and rationale you can [watch the talk given at ClojureX Bytes](
 
 ## Clojure / ClojureScript
 
-Given a [Swagger API definition](https://pedestal-api.herokuapp.com/swagger.json)
+Given an [OpenAPI/Swagger API definition](https://pedestal-api.herokuapp.com/swagger.json)
 like that provided by [pedestal-api](https://github.com/oliyh/pedestal-api):
 
 ```clojure
 (require '[martian.core :as martian]
          '[martian.clj-http :as martian-http])
 
-;; bootstrap the martian instance by simply providing the url serving the swagger description
-(let [m (martian-http/bootstrap-swagger "https://pedestal-api.herokuapp.com/swagger.json")]
+;; bootstrap the martian instance by simply providing the url serving the openapi/swagger description
+(let [m (martian-http/bootstrap-openapi "https://pedestal-api.herokuapp.com/swagger.json")]
 
   ;; explore the endpoints
   (martian/explore m)
@@ -116,9 +116,9 @@ like that provided by [pedestal-api](https://github.com/oliyh/pedestal-api):
 
 ## No Swagger, no problem
 
-Although bootstrapping against a remote Swagger API using `bootstrap-swagger` is simplest
+Although bootstrapping against a remote OpenAPI or Swagger API using `bootstrap-openapi` is simplest
 and allows you to use the golden source to define the API, you may likely find yourself
-needing to integrate with an API beyond your control which does not use Swagger.
+needing to integrate with an API beyond your control which does not use OpenAPI or Swagger.
 
 Martian offers a separate `bootstrap` function which you can provide with handlers defined as data.
 Here's an example:
@@ -154,7 +154,7 @@ The following example shows how exceptions will be thrown by bad code and how re
          '[martian.httpkit :as martian-http]
          '[martian.test :as martian-test])
 
-(let [m (-> (martian-http/bootstrap-swagger "https://pedestal-api.herokuapp.com/swagger.json")
+(let [m (-> (martian-http/bootstrap-openapi "https://pedestal-api.herokuapp.com/swagger.json")
             (martian-test/respond-with-generated {:get-pet :random}))]
 
   (martian/response-for m :get-pet {})
@@ -234,7 +234,7 @@ For example, if you wish to add an authentication header and a timer to all requ
                  (println))
             ctx)})
 
-(let [m (martian-http/bootstrap-swagger
+(let [m (martian-http/bootstrap-openapi
                "https://pedestal-api.herokuapp.com/swagger.json"
                {:interceptors (concat martian/default-interceptors
                                       [add-authentication-header
@@ -264,10 +264,10 @@ option exists using `bootstrap` and providing `:interceptors` as follows:
                                      :enter #(assoc-in % [:request :method] :xget)}]}])
 ```
 
-Alternatively you can use the helpers like `update-handler` to update a martian created from `bootstrap-swagger`:
+Alternatively you can use the helpers like `update-handler` to update a martian created from `bootstrap-openapi`:
 
 ```clojure
-(-> (martian/bootstrap-swagger "https://api.org" swagger-definition)
+(-> (martian/bootstrap-openapi "https://api.org" openapi-definition)
     (martian/update-handler :load-pet assoc :interceptors [{:name ::override-load-pet-method
                                                             :enter #(assoc-in % [:request :method] :xget)}]))
 ```
@@ -306,7 +306,7 @@ Martian allows you to add support for content-types in addition to those support
                       "application/magical" {:encode magic-encoder
                                              :decode magic-decoder
                                              :as :magic})]
-  (http/bootstrap-swagger
+  (http/bootstrap-openapi
    "https://example-api.com"
    {:interceptors (concat m/default-interceptors
                           [(i/encode-body encoders)
@@ -331,17 +331,17 @@ martian.urlFor("get-pet", new HashMap<String, Object> {{ put("id", 123); }});
 ```
 
 ## Caveats
-- You need `:operationId` in the Swagger spec to name routes when using `bootstrap-swagger`
+- You need `:operationId` in the OpenAPI/Swagger spec to name routes when using `bootstrap-openapi`
   - [pedestal-api](https://github.com/oliyh/pedestal-api) automatically generates these from the route name
 
 ## Development
 [![Circle CI](https://circleci.com/gh/oliyh/martian.svg?style=svg)](https://circleci.com/gh/oliyh/martian)
 
 Use `cider-jack-in-clj` or `cider-jack-in-clj&cljs` to start Clojure (and Clojurescript where appropriate) REPLs for development.
+You may need to `lein install` first if you're working in a module that depends on another.
 
 ## Issues and features
 Please feel free to raise issues on Github or send pull requests.
 
 ## Acknowledgements
 Martian uses [tripod](https://github.com/frankiesardo/tripod) for routing, inspired by [pedestal](https://github.com/pedestal/pedestal).
-See also [fintrospect](http://fintrospect.io/) and [kekkonen](https://github.com/metosin/kekkonen) for ideas integrating server and client beyond Swagger.
diff --git a/clj-http-lite/src/martian/clj_http_lite.clj b/clj-http-lite/src/martian/clj_http_lite.clj
index 6c92248..a419b2c 100644
--- a/clj-http-lite/src/martian/clj_http_lite.clj
+++ b/clj-http-lite/src/martian/clj_http_lite.clj
@@ -2,7 +2,8 @@
   (:require [clj-http.lite.client :as http]
             [cheshire.core :as json]
             [martian.core :as martian]
-            [martian.interceptors :as interceptors]))
+            [martian.interceptors :as interceptors]
+            [martian.openapi :refer [openapi-schema?] :as openapi]))
 
 (defn- prepare-response-headers [headers]
   (reduce (fn [m [k v]] (assoc m (keyword k) v)) {} headers))
@@ -17,15 +18,18 @@
 (def default-interceptors
   (concat martian/default-interceptors [interceptors/default-encode-body interceptors/default-coerce-response perform-request]))
 
+(def default-opts {:interceptors default-interceptors})
+
 (defn bootstrap [api-root concise-handlers & [opts]]
-  (martian/bootstrap api-root concise-handlers (merge {:interceptors default-interceptors} opts)))
+  (martian/bootstrap api-root concise-handlers (merge default-opts opts)))
 
-(defn bootstrap-swagger [url & [{:keys [interceptors] :as params}]]
-  ;; clj-http-lite does not support {:as :json} body conversion (yet) so we do it right here
-  (let [swagger-definition (-> (http/get url) :body (json/parse-string keyword))
+(defn bootstrap-openapi [url & [opts]]
+  (let [definition (-> (http/get url) :body (json/parse-string keyword)) ;; clj-http-lite does not support {:as :json} body conversion (yet) so we do it right here
         {:keys [scheme server-name server-port]} (http/parse-url url)
-        base-url (format "%s://%s%s%s" (name scheme) server-name (if server-port (str ":" server-port) "") (get swagger-definition :basePath ""))]
-    (martian/bootstrap-swagger
-     base-url
-     swagger-definition
-     {:interceptors (or interceptors default-interceptors)})))
+        base-url (format "%s://%s%s%s" (name scheme) server-name (if server-port (str ":" server-port) "")
+                         (if (openapi-schema? definition)
+                           (openapi/base-url definition)
+                           (get definition :basePath "")))]
+    (martian/bootstrap-openapi base-url definition (merge default-opts opts))))
+
+(def bootstrap-swagger bootstrap-openapi)
diff --git a/clj-http/src/martian/clj_http.clj b/clj-http/src/martian/clj_http.clj
index f4a88ad..f78349a 100644
--- a/clj-http/src/martian/clj_http.clj
+++ b/clj-http/src/martian/clj_http.clj
@@ -1,9 +1,8 @@
 (ns martian.clj-http
   (:require [clj-http.client :as http]
-            [cheshire.core :as json]
-            [cognitect.transit :as transit]
             [martian.core :as martian]
-            [martian.interceptors :as interceptors])
+            [martian.interceptors :as interceptors]
+            [martian.openapi :refer [openapi-schema?] :as openapi])
   (:import [java.io ByteArrayInputStream]))
 
 (def perform-request
@@ -14,14 +13,18 @@
 (def default-interceptors
   (concat martian/default-interceptors [interceptors/default-encode-body interceptors/default-coerce-response perform-request]))
 
+(def default-opts {:interceptors default-interceptors})
+
 (defn bootstrap [api-root concise-handlers & [opts]]
-  (martian/bootstrap api-root concise-handlers (merge {:interceptors default-interceptors} opts)))
+  (martian/bootstrap api-root concise-handlers (merge default-opts opts)))
 
-(defn bootstrap-swagger [url & [{:keys [interceptors] :as params}]]
-  (let [swagger-definition (:body (http/get url {:as :json}))
+(defn bootstrap-openapi [url & [opts]]
+  (let [definition (:body (http/get url {:as :json}))
         {:keys [scheme server-name server-port]} (http/parse-url url)
-        base-url (format "%s://%s%s%s" (name scheme) server-name (if server-port (str ":" server-port) "") (get swagger-definition :basePath ""))]
-    (martian/bootstrap-swagger
-     base-url
-     swagger-definition
-     {:interceptors (or interceptors default-interceptors)})))
+        base-url (format "%s://%s%s%s" (name scheme) server-name (if server-port (str ":" server-port) "")
+                         (if (openapi-schema? definition)
+                           (openapi/base-url definition)
+                           (get definition :basePath "")))]
+    (martian/bootstrap-openapi base-url definition (merge default-opts opts))))
+
+(def bootstrap-swagger bootstrap-openapi)
diff --git a/cljs-http/src/martian/cljs_http.cljs b/cljs-http/src/martian/cljs_http.cljs
index 1bdb081..4d8b818 100644
--- a/cljs-http/src/martian/cljs_http.cljs
+++ b/cljs-http/src/martian/cljs_http.cljs
@@ -3,6 +3,7 @@
             [cljs.core.async :refer [<!]]
             [martian.core :as martian]
             [martian.interceptors :as i]
+            [martian.openapi :refer [openapi-schema?] :as openapi]
             [tripod.context :as tc]
             [clojure.string :as str])
   (:require-macros [cljs.core.async.macros :refer [go]]))
@@ -22,19 +23,22 @@
 (def default-interceptors
   (concat martian/default-interceptors [i/default-encode-body i/default-coerce-response perform-request]))
 
+(def default-opts {:interceptors default-interceptors})
+
 (defn bootstrap [api-root concise-handlers & [opts]]
-  (martian/bootstrap api-root concise-handlers (merge {:interceptors default-interceptors} opts)))
+  (martian/bootstrap api-root concise-handlers (merge default-opts opts)))
 
-(defn bootstrap-swagger [url & [{:keys [interceptors trim-base-url?] :as params}]]
-  (go (let [swagger-definition (:body (<! (http/get url {:as :json})))
+(defn bootstrap-openapi [url & [{:keys [trim-base-url?] :as opts}]]
+  (go (let [definition (:body (<! (http/get url {:as :json})))
             {:keys [scheme server-name server-port]} (http/parse-url url)
             raw-base-url (str (when-not (re-find #"^/" url)
                                 (str (name scheme) "://" server-name (when server-port (str ":" server-port))))
-                              (get swagger-definition :basePath ""))
+                              (if (openapi-schema? definition)
+                                (openapi/base-url definition)
+                                (get definition :basePath "")))
             base-url (if trim-base-url?
                        (str/replace raw-base-url #"/$" "")
                        raw-base-url)]
-        (martian/bootstrap-swagger
-         base-url
-         swagger-definition
-         {:interceptors (or interceptors default-interceptors)}))))
+        (martian/bootstrap-openapi base-url definition (merge default-opts opts)))))
+
+(def bootstrap-swagger bootstrap-openapi)
diff --git a/core/src/martian/core.cljc b/core/src/martian/core.cljc
index e2e19a6..89e9625 100644
--- a/core/src/martian/core.cljc
+++ b/core/src/martian/core.cljc
@@ -5,7 +5,8 @@
             [clojure.walk :refer [keywordize-keys postwalk-replace]]
             [martian.interceptors :as interceptors]
             [martian.schema :as schema]
-            [martian.swagger :as swagger]
+            [martian.swagger :refer [swagger->handlers]]
+            [martian.openapi :refer [openapi->handlers openapi-schema?]]
             [schema.core :as s]
             [clojure.spec.alpha :as spec]
             [martian.spec :as mspec]))
@@ -48,7 +49,7 @@
   "Update a handler in the martian record with the provided route-name
    e.g. add route-specific interceptors:
    (update-handler m :load-pet assoc :interceptors [an-interceptor])"
-  [{:keys [handlers] :as m} route-name update-fn & update-args]
+  [m route-name update-fn & update-args]
   (update m :handlers #(mapv (fn [handler]
                                (if (= (keyword route-name) (:route-name handler))
                                  (apply update-fn handler update-args)
@@ -109,15 +110,17 @@
                   :handlers (spec/coll-of ::mspec/handler)
                   :opts ::mspec/opts))
 
-(defn bootstrap-swagger
-  "Creates a martian instance from a swagger spec
-
-   (let [m (bootstrap-swagger \"https://api.org\" swagger-spec)]
-     (url-for m :load-pet {:id 123}))
-
-   ;; => https://api.org/pets/123"
-  [api-root swagger-json & [opts]]
-  (build-instance api-root (map enrich-handler (swagger/swagger->handlers swagger-json)) (keywordize-keys opts)))
+(defn bootstrap-openapi
+  "Creates a martian instance from an openapi/swagger spec based on the schema provided"
+  [api-root json & [opts]]
+  (let [{:keys [interceptors] :or {interceptors default-interceptors} :as opts} (keywordize-keys opts)]
+    (build-instance api-root
+                    (map enrich-handler (if (openapi-schema? json)
+                                          (openapi->handlers json (interceptors/supported-content-types interceptors))
+                                          (swagger->handlers json)))
+                    opts)))
+
+(def bootstrap-swagger bootstrap-openapi)
 
 (defn bootstrap
   "Creates a martian instance from a martian description"
diff --git a/core/src/martian/interceptors.cljc b/core/src/martian/interceptors.cljc
index da060fd..89ed5a1 100644
--- a/core/src/martian/interceptors.cljc
+++ b/core/src/martian/interceptors.cljc
@@ -1,7 +1,6 @@
 (ns martian.interceptors
   (:require [martian.schema :as schema]
             [clojure.walk :refer [keywordize-keys stringify-keys]]
-            [clojure.string :as string]
             [tripod.context :as tc]
             [schema.core :as s]
             [camel-snake-kebab.core :refer [->kebab-case-keyword]]
@@ -79,11 +78,12 @@
 
 (defn encode-body [encoders]
   {:name ::encode-body
+   :encodes (keys encoders)
    :enter (fn [{:keys [request handler] :as ctx}]
             (let [content-type (and (:body request)
                                     (not (get-in request [:headers "Content-Type"]))
                                     (encoding/choose-content-type encoders (:consumes handler)))
-                  {:keys [encode] :as encoder} (encoding/find-encoder encoders content-type)]
+                  {:keys [encode]} (encoding/find-encoder encoders content-type)]
               (cond-> ctx
                 (get-in ctx [:request :body]) (update-in [:request :body] encode)
                 content-type (assoc-in [:request :headers "Content-Type"] content-type))))})
@@ -92,6 +92,7 @@
 
 (defn coerce-response [encoders]
   {:name ::coerce-response
+   :decodes (keys encoders)
    :enter (fn [{:keys [request handler] :as ctx}]
             (let [content-type (and (not (get-in request [:headers "Accept"]))
                                     (encoding/choose-content-type encoders (:produces handler)))
@@ -100,11 +101,20 @@
               (cond-> (assoc-in ctx [:request :as] as)
                 content-type (assoc-in [:request :headers "Accept"] content-type))))
 
-   :leave (fn [{:keys [request response handler] :as ctx}]
+   :leave (fn [{:keys [response] :as ctx}]
             (assoc ctx :response
                    (let [content-type (and (:body response)
                                            (not-empty (get-in response [:headers :content-type])))
-                         {:keys [matcher decode] :as encoder} (encoding/find-encoder encoders content-type)]
+                         {:keys [decode]} (encoding/find-encoder encoders content-type)]
                      (update response :body decode))))})
 
 (def default-coerce-response (coerce-response (encoders/default-encoders)))
+
+(defn supported-content-types
+  "Return the full set of supported content-types as declared by any encoding/decoding interceptors"
+  [interceptors]
+  (reduce (fn [acc interceptor]
+            (merge-with into acc (select-keys interceptor [:encodes :decodes])))
+          {:encodes #{}
+           :decodes #{}}
+          interceptors))
diff --git a/core/src/martian/openapi.cljc b/core/src/martian/openapi.cljc
index 9769348..1505564 100644
--- a/core/src/martian/openapi.cljc
+++ b/core/src/martian/openapi.cljc
@@ -1,17 +1,19 @@
 (ns martian.openapi
   (:require [camel-snake-kebab.core :refer [->kebab-case-keyword]]
             [clojure.string :as string]
-            [schema.core :as s]))
+            [clojure.walk :refer [keywordize-keys]]
+            [schema.core :as s]
+            #?(:cljs [cljs.reader :refer [read-string]])))
 
-(defn- tokenise-path [url-pattern]
-  (for [part (re-seq #"[^{]+|\{[^}]+?\}" url-pattern)]
-    (if-let [param (re-matches #"^\{(.*)\}$" part)]
-      (keyword (second param))
-      part)))
+(defn openapi-schema? [json]
+  (some #(get json %) [:openapi "openapi"]))
+
+(defn base-url [json]
+  (get-in json [:servers 0 :url] ""))
 
 (defn- lookup-ref [components reference]
   (if (string/starts-with? reference "#/components/")
-    (or (get-in components (drop 2 (string/split reference #"/")))
+    (or (get-in components (drop 2 (map keyword (string/split reference #"/"))))
         (throw (ex-info "Cannot find reference"
                         {:reference reference})))
     (throw (ex-info "References start with something other than #/components/ aren't supported yet. :("
@@ -22,51 +24,51 @@
      :cljs goog.Uri))
 
 (defn- openapi->schema
-  ([schema components]
-   (openapi->schema schema components #{}))
+  ([schema components] (openapi->schema schema components #{}))
   ([schema components seen-set]
-   (if-let [reference (get schema "$ref")]
+   (if-let [reference (:$ref schema)]
      (if (contains? seen-set reference)
        s/Any ; If we've already seen this, then we're in a loop. Rather than
              ; trying to solve for the fixpoint, just return Any.
        (recur (lookup-ref components reference)
               components
               (conj seen-set reference)))
-     (let [wrap (if (get schema "nullable") s/maybe identity)]
+     (let [wrap (if (:nullable schema) s/maybe identity)]
        (wrap
-        (condp = (get schema "type")
-          "string"  (if-let [enum (get schema "enum")]
+        (condp = (:type schema)
+          "string"  (if-let [enum (:enum schema)]
                       (apply s/enum enum)
-                      (condp = (get schema "format")
+                      (condp = (:format schema)
                         "uuid" s/Uuid
                         "uri" URI
                         s/Str))
           "integer" s/Int
           "number"  s/Num
           "boolean" s/Bool
-          "array"   [(openapi->schema (get schema "items") components seen-set)]
-          "object"  (let [required? (set (get schema "required"))]
+          "array"   [(openapi->schema (:items schema) components seen-set)]
+          "object"  (let [required? (set (:required schema))]
                       (into {}
                             (map (fn [[k v]]
-                                   {(if (required? k)
+                                   {(if (required? (name k))
                                       (keyword k)
                                       (s/optional-key (keyword k)))
                                     (openapi->schema v components seen-set)}))
-                            (get schema "properties")))
+                            (:properties schema)))
           (throw (ex-info "Cannot convert OpenAPI type to schema" {:definition schema}))))))))
 
 (defn- get-matching-schema [object content-types]
-  (if-let [content-type (first (filter #(get-in object ["content" % "schema"]) content-types))]
-    [(get-in object ["content" content-type "schema"])
+  (if-let [content-type (first (filter #(get-in object [:content (keyword %) :schema]) content-types))]
+    [(get-in object [:content (keyword content-type) :schema])
      content-type]
-    (when (seq (get object "content"))
-      (throw (ex-info "No matching content-type available"
-                      {:allowed-content-types  content-types
-                       :response-content-types (keys (get object "content"))})))))
+    (when (seq (:content object))
+      #?(:clj (println "No matching content-type available" {:supported-content-types content-types
+                                                             :available-content-types (map name (keys (:content object)))})
+         :cljs (js/console.warn "No matching content-type available" {:supported-content-types content-types
+                                                                      :available-content-types (keys (get object "content"))})))))
 
 (defn- process-body [body components content-types]
   (when-let [[json-schema content-type] (get-matching-schema body content-types)]
-    (let [required (get body "required")]
+    (let [required (:required body)]
       {:schema       {(if required :body (s/optional-key :body))
                       (openapi->schema json-schema components)}
        :content-type content-type})))
@@ -74,43 +76,59 @@
 (defn- process-parameters [parameters components]
   (into {}
         (map (fn [param]
-               {(if (get param "required")
-                  (keyword (get param "name"))
-                  (s/optional-key (keyword (get param "name"))))
-                (openapi->schema (get param "schema") components)}))
+               {(if (:required param)
+                  (keyword (:name param))
+                  (s/optional-key (keyword (:name param))))
+                (openapi->schema (:schema param) components)}))
         parameters))
 
 (defn- process-responses [responses components content-types]
-  (for [[code value] responses
-        :let         [[json-schema content-type] (get-matching-schema value content-types)]]
-    {:status       (if (= code "default")
+  (for [[status-code value] responses
+        :let                [status-code (name status-code)
+                             [json-schema content-type] (get-matching-schema value content-types)]]
+    {:status       (if (= status-code "default")
                      s/Any
-                     (s/eq (Long/parseLong code)))
+                     (s/eq (if (number? status-code) status-code (read-string (name status-code)))))
      :body         (and json-schema (openapi->schema json-schema components))
      :content-type content-type}))
 
-(defn openapi->handlers [swagger-json content-types]
-  (let [components (get swagger-json "components")]
-    (for [[url methods] (get swagger-json "paths")
+(defn- sanitise [x]
+  (if (string? x)
+    x
+    ;; consistent across clj and cljs
+    (-> (str x)
+        (string/replace-first ":" ""))))
+
+(defn tokenise-path [url-pattern]
+  (let [url-pattern (sanitise url-pattern)
+        parts (map first (re-seq #"([^{}]+|\{.+?\})" url-pattern))]
+    (map #(if-let [param-name (second (re-matches #"^\{(.*)\}" %))]
+            (keyword param-name)
+            %) parts)))
+
+(defn openapi->handlers [openapi-json content-types]
+  (let [openapi-spec (keywordize-keys openapi-json)
+        components (:components openapi-spec)]
+    (for [[url methods] (:paths openapi-spec)
           [method definition] methods
           ;; We only care about things which have a defined operationId, and
           ;; which aren't the associated OPTIONS call.
-          :when (and (get definition "operationId")
-                     (not= method "options"))
-          :let [parameters (group-by #(get % "in") (get definition "parameters"))
-                body       (process-body (get definition "requestBody") components content-types)
-                responses  (process-responses (get definition "responses") components content-types)]]
+          :when (and (:operationId definition)
+                     (not= :options method))
+          :let [parameters (group-by :in (:parameters definition))
+                body       (process-body (:requestBody definition) components (:encodes content-types))
+                responses  (process-responses (:responses definition) components (:decodes content-types))]]
       {:path-parts         (vec (tokenise-path url))
-       :method             (keyword method)
-       :path-schema        (process-parameters (get parameters "path") components)
-       :query-schema       (process-parameters (get parameters "query") components)
+       :method             method
+       :path-schema        (process-parameters (:path parameters) components)
+       :query-schema       (process-parameters (:query parameters) components)
        :body-schema        (:schema body)
-       :form-schema        (process-parameters (get parameters "form") components)
-       :headers-schema     (process-parameters (get parameters "header") components)
+       :form-schema        (process-parameters (:form parameters) components)
+       :headers-schema     (process-parameters (:header parameters) components)
        :response-schemas   (vec (keep #(dissoc % :content-type) responses))
        :produces           (vec (keep :content-type responses))
        :consumes           [(:content-type body)]
-       :summary            (get definition "summary")
-       :description        (get definition "description")
+       :summary            (:summary definition)
+       :description        (:description definition)
        :openapi-definition definition
-       :route-name         (->kebab-case-keyword (get definition "operationId"))})))
+       :route-name         (->kebab-case-keyword (:operationId definition))})))
diff --git a/core/src/martian/swagger.cljc b/core/src/martian/swagger.cljc
index a98acbe..239ebf7 100644
--- a/core/src/martian/swagger.cljc
+++ b/core/src/martian/swagger.cljc
@@ -1,5 +1,6 @@
 (ns martian.swagger
-  (:require [camel-snake-kebab.core :refer [->kebab-case-keyword]]
+  (:require [martian.openapi :refer [tokenise-path]]
+            [camel-snake-kebab.core :refer [->kebab-case-keyword]]
             [clojure.string :as string]
             [clojure.walk :refer [keywordize-keys]]
             [martian.schema :as schema]
@@ -32,20 +33,6 @@
     {:status (s/eq status-code)
      :body (schema/make-schema ref-lookup (assoc (:schema response) :required true))}))
 
-(defn- sanitise [x]
-  (if (string? x)
-    x
-    ;; consistent across clj and cljs
-    (-> (str x)
-        (string/replace-first ":" ""))))
-
-(defn- tokenise-path [url-pattern]
-  (let [url-pattern (sanitise url-pattern)
-        parts (map first (re-seq #"([^{}]+|\{.+?\})" url-pattern))]
-    (map #(if-let [param-name (second (re-matches #"^\{(.*)\}" %))]
-            (keyword param-name)
-            %) parts)))
-
 (defn- ->handler
   [swagger-map
    path-item-parameters
diff --git a/httpkit/src/martian/httpkit.clj b/httpkit/src/martian/httpkit.clj
index 002cf8a..705928c 100644
--- a/httpkit/src/martian/httpkit.clj
+++ b/httpkit/src/martian/httpkit.clj
@@ -2,6 +2,7 @@
   (:require [org.httpkit.client :as http]
             [martian.core :as martian]
             [martian.interceptors :as interceptors]
+            [martian.openapi :refer [openapi-schema?] :as openapi]
             [cheshire.core :as json]
             [tripod.context :as tc])
   (:import [java.net URL]))
@@ -32,14 +33,18 @@
                                         interceptors/default-coerce-response
                                         perform-request]))
 
+(def default-opts {:interceptors default-interceptors})
+
 (defn bootstrap [api-root concise-handlers & [opts]]
-  (martian/bootstrap api-root concise-handlers (merge {:interceptors default-interceptors} opts)))
+  (martian/bootstrap api-root concise-handlers (merge default-opts opts)))
 
-(defn bootstrap-swagger [url & [{:keys [interceptors] :as params}]]
-  (let [swagger-definition @(http/get url {:as :text} (fn [{:keys [body]}] (json/decode body keyword)))
+(defn bootstrap-openapi [url & [opts]]
+  (let [definition @(http/get url {:as :text} (fn [{:keys [body]}] (json/decode body keyword)))
         {:keys [scheme server-name server-port]} (parse-url url)
-        base-url (format "%s://%s%s%s" (name scheme) server-name (if server-port (str ":" server-port) "") (get swagger-definition :basePath ""))]
-    (martian/bootstrap-swagger
-     base-url
-     swagger-definition
-     {:interceptors (or interceptors default-interceptors)})))
+        base-url (format "%s://%s%s%s" (name scheme) server-name (if server-port (str ":" server-port) "")
+                         (if (openapi-schema? definition)
+                           (openapi/base-url definition)
+                           (get definition :basePath "")))]
+    (martian/bootstrap-openapi base-url definition (merge default-opts opts))))
+
+(def bootstrap-swagger bootstrap-openapi)
diff --git a/re-frame/src/martian/re_frame.cljs b/re-frame/src/martian/re_frame.cljs
index 2b8ad9f..7c1e75a 100644
--- a/re-frame/src/martian/re_frame.cljs
+++ b/re-frame/src/martian/re_frame.cljs
@@ -60,5 +60,5 @@
    (get-in db [::martian :pending])))
 
 (defn init [swagger-url & [params]]
-  (go (let [martian (<! (martian-http/bootstrap-swagger swagger-url params))]
+  (go (let [martian (<! (martian-http/bootstrap-openapi swagger-url params))]
         (re-frame/dispatch-sync [::init martian]))))


Test patch:
diff --git a/clj-http-lite/test/martian/clj_http_lite_test.clj b/clj-http-lite/test/martian/clj_http_lite_test.clj
index 12e4c61..cf3a50c 100644
--- a/clj-http-lite/test/martian/clj_http_lite_test.clj
+++ b/clj-http-lite/test/martian/clj_http_lite_test.clj
@@ -2,16 +2,13 @@
   (:require [martian.clj-http-lite :as martian-http]
             [martian.core :as martian]
             [martian.encoders :as encoders]
-            [tripod.context :as tc]
-            [martian.interceptors :as i]
-            [martian.server-stub :refer [with-server swagger-url]]
+            [martian.server-stub :refer [with-server swagger-url openapi-url]]
             [martian.test-utils :refer [input-stream->byte-array]]
-            [clojure.test :refer :all]))
+            [clojure.test :refer [deftest testing is use-fixtures]]))
 
 (use-fixtures :once with-server)
 
-(deftest http-test
-
+(deftest swagger-http-test
   (let [m (martian-http/bootstrap-swagger swagger-url)]
     (testing "default encoders"
       (is (= {:method :post
@@ -36,4 +33,13 @@
       (is (= {:name "Doggy McDogFace"
               :type "Dog"
               :age 3}
-             (:body response))))))
\ No newline at end of file
+             (:body response))))))
+
+(deftest openapi-bootstrap-test
+  (let [m (martian-http/bootstrap-openapi openapi-url)]
+
+    (is (= "http://localhost:8888/openapi/v3"
+           (:api-root m)))
+
+    (is (contains? (set (map first (martian/explore m)))
+                   :get-order-by-id))))
diff --git a/clj-http/test/martian/clj_http_test.clj b/clj-http/test/martian/clj_http_test.clj
index bfff73e..72403d5 100644
--- a/clj-http/test/martian/clj_http_test.clj
+++ b/clj-http/test/martian/clj_http_test.clj
@@ -2,13 +2,13 @@
   (:require [martian.clj-http :as martian-http]
             [martian.core :as martian]
             [martian.encoders :as encoders]
-            [martian.server-stub :refer [with-server swagger-url]]
+            [martian.server-stub :refer [with-server swagger-url openapi-url]]
             [martian.test-utils :refer [input-stream->byte-array]]
-            [clojure.test :refer :all]))
+            [clojure.test :refer [deftest testing is use-fixtures]]))
 
 (use-fixtures :once with-server)
 
-(deftest http-test
+(deftest swagger-http-test
   (let [m (martian-http/bootstrap-swagger swagger-url)]
 
     (testing "default encoders"
@@ -35,3 +35,12 @@
               :type "Dog"
               :age 3}
              (:body response))))))
+
+(deftest openapi-bootstrap-test
+  (let [m (martian-http/bootstrap-openapi openapi-url)]
+
+    (is (= "http://localhost:8888/openapi/v3"
+           (:api-root m)))
+
+    (is (contains? (set (map first (martian/explore m)))
+                   :get-order-by-id))))
diff --git a/cljs-http/test/martian/cljs_http_test.cljs b/cljs-http/test/martian/cljs_http_test.cljs
index 2342813..de60fd9 100644
--- a/cljs-http/test/martian/cljs_http_test.cljs
+++ b/cljs-http/test/martian/cljs_http_test.cljs
@@ -5,7 +5,7 @@
             [cljs.core.async :refer [<! timeout]])
   (:require-macros [cljs.core.async.macros :refer [go]]))
 
-(deftest http-test
+(deftest swagger-http-test
   (async done
          (go (let [m (<! (martian-http/bootstrap-swagger "http://localhost:8888/swagger.json"))]
 
@@ -22,3 +22,14 @@
                          :age 3}
                         (:body response)))))
              (done))))
+
+(deftest openapi-bootstrap-test
+  (async done
+         (go (let [m (<! (martian-http/bootstrap-openapi "http://localhost:8888/openapi.json"))]
+
+               (is (= "http://localhost:8888/openapi/v3"
+                      (:api-root m)))
+
+               (is (contains? (set (map first (martian/explore m)))
+                              :get-order-by-id))))
+         (done)))
diff --git a/core/test-resources/swagger.json b/core/test-resources/swagger.json
index abd1af6..6d6d1e1 100644
--- a/core/test-resources/swagger.json
+++ b/core/test-resources/swagger.json
@@ -1,1 +1,366 @@
-{"swagger":"2.0","info":{"title":"Swagger Sample App built using pedestal-api","version":"2.0","description":"Find out more at https://github.com/oliyh/pedestal-api"},"produces":["application/json"],"consumes":["application/json"],"paths":{"/pets/":{"get":{"responses":{"400":{"description":"The request contains bad syntax or cannot be fulfilled."},"500":{"description":"There was an internal server error."},"200":{"schema":{"$ref":"#/definitions/Response39145"},"description":"OK"}},"produces":["application/json","application/edn","application/transit+json","application/transit+msgpack","application/transit"],"consumes":["application/json","application/edn","application/x-www-form-urlencoded","application/transit+json","application/transit+msgpack"],"tags":["pets"],"summary":"Get all pets in the store","parameters":[{"in":"query","name":"sort","description":"","required":false,"type":"string","enum":["desc","asc"]}],"operationId":"all-pets"},"post":{"responses":{"400":{"description":"The request contains bad syntax or cannot be fulfilled."},"500":{"description":"There was an internal server error."},"201":{"schema":{"$ref":"#/definitions/Response39146"},"description":"The request has been fulfilled and resulted in a new resource being created."}},"produces":["application/json","application/edn","application/transit+json","application/transit+msgpack","application/transit"],"consumes":["application/json","application/edn","application/x-www-form-urlencoded","application/transit+json","application/transit+msgpack"],"tags":["pets"],"summary":"Create a pet","parameters":[{"in":"body","name":"Pet","description":"","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"operationId":"create-pet"}},"/pets/{id}":{"get":{"responses":{"400":{"description":"The request contains bad syntax or cannot be fulfilled."},"500":{"description":"There was an internal server error."},"404":{"schema":{"type":"string"},"description":"The requested resource could not be found but may be available again in the future."},"200":{"schema":{"$ref":"#/definitions/PetWithId"},"description":"OK"}},"produces":["application/json","application/edn","application/transit+json","application/transit+msgpack","application/transit"],"consumes":["application/json","application/edn","application/x-www-form-urlencoded","application/transit+json","application/transit+msgpack"],"tags":["pets"],"summary":"Get a pet by id","parameters":[{"in":"path","name":"id","description":"","required":true,"type":"string","format":"uuid"}],"operationId":"get-pet"},"put":{"responses":{"400":{"description":"The request contains bad syntax or cannot be fulfilled."},"500":{"description":"There was an internal server error."},"404":{"schema":{"type":"string"},"description":"The requested resource could not be found but may be available again in the future."},"200":{"schema":{"type":"string"},"description":"OK"}},"produces":["application/json","application/edn","application/transit+json","application/transit+msgpack","application/transit"],"consumes":["application/json","application/edn","application/x-www-form-urlencoded","application/transit+json","application/transit+msgpack"],"tags":["pets"],"summary":"Update a pet","parameters":[{"in":"path","name":"id","description":"","required":true,"type":"string","format":"uuid"},{"in":"body","name":"Pet","description":"","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"operationId":"update-pet"},"delete":{"responses":{"400":{"description":"The request contains bad syntax or cannot be fulfilled."},"500":{"description":"There was an internal server error."},"404":{"schema":{"type":"string"},"description":"The requested resource could not be found but may be available again in the future."},"200":{"schema":{"type":"string"},"description":"OK"}},"produces":["application/json","application/edn","application/transit+json","application/transit+msgpack","application/transit"],"consumes":["application/json","application/edn","application/x-www-form-urlencoded","application/transit+json","application/transit+msgpack"],"tags":["pets"],"summary":"Delete a pet by id","parameters":[{"in":"path","name":"id","description":"","required":true,"type":"string","format":"uuid"}],"operationId":"delete-pet"}}},"tags":[{"name":"pets","description":"Everything about your Pets","externalDocs":{"description":"Find out more","url":"http://swagger.io"}},{"name":"orders","description":"Operations about orders"}],"definitions":{"Pet":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"age":{"type":"integer","format":"int64"}},"additionalProperties":false,"required":["name","type","age"]},"PetWithId":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"age":{"type":"integer","format":"int64"},"id":{"type":"string","format":"uuid"}},"additionalProperties":false,"required":["name","type","age"]},"Response39145":{"type":"object","properties":{"pets":{"type":"array","items":{"$ref":"#/definitions/PetWithId"}}},"additionalProperties":false,"required":["pets"]},"Response39146":{"type":"object","properties":{"id":{"type":"string","format":"uuid"}},"additionalProperties":false,"required":["id"]}}}
+{
+  "swagger": "2.0",
+  "info": {
+    "title": "Swagger Sample App built using pedestal-api",
+    "version": "2.0",
+    "description": "Find out more at https://github.com/oliyh/pedestal-api"
+  },
+  "produces": [
+    "application/json"
+  ],
+  "consumes": [
+    "application/json"
+  ],
+  "paths": {
+    "/pets/": {
+      "get": {
+        "responses": {
+          "400": {
+            "description": "The request contains bad syntax or cannot be fulfilled."
+          },
+          "500": {
+            "description": "There was an internal server error."
+          },
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Response39145"
+            },
+            "description": "OK"
+          }
+        },
+        "produces": [
+          "application/json",
+          "application/edn",
+          "application/transit+json",
+          "application/transit+msgpack",
+          "application/transit"
+        ],
+        "consumes": [
+          "application/json",
+          "application/edn",
+          "application/x-www-form-urlencoded",
+          "application/transit+json",
+          "application/transit+msgpack"
+        ],
+        "tags": [
+          "pets"
+        ],
+        "summary": "Get all pets in the store",
+        "parameters": [
+          {
+            "in": "query",
+            "name": "sort",
+            "description": "",
+            "required": false,
+            "type": "string",
+            "enum": [
+              "desc",
+              "asc"
+            ]
+          }
+        ],
+        "operationId": "all-pets"
+      },
+      "post": {
+        "responses": {
+          "400": {
+            "description": "The request contains bad syntax or cannot be fulfilled."
+          },
+          "500": {
+            "description": "There was an internal server error."
+          },
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/Response39146"
+            },
+            "description": "The request has been fulfilled and resulted in a new resource being created."
+          }
+        },
+        "produces": [
+          "application/json",
+          "application/edn",
+          "application/transit+json",
+          "application/transit+msgpack",
+          "application/transit"
+        ],
+        "consumes": [
+          "application/json",
+          "application/edn",
+          "application/x-www-form-urlencoded",
+          "application/transit+json",
+          "application/transit+msgpack"
+        ],
+        "tags": [
+          "pets"
+        ],
+        "summary": "Create a pet",
+        "parameters": [
+          {
+            "in": "body",
+            "name": "Pet",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/Pet"
+            }
+          }
+        ],
+        "operationId": "create-pet"
+      }
+    },
+    "/pets/{id}": {
+      "get": {
+        "responses": {
+          "400": {
+            "description": "The request contains bad syntax or cannot be fulfilled."
+          },
+          "500": {
+            "description": "There was an internal server error."
+          },
+          "404": {
+            "schema": {
+              "type": "string"
+            },
+            "description": "The requested resource could not be found but may be available again in the future."
+          },
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/PetWithId"
+            },
+            "description": "OK"
+          }
+        },
+        "produces": [
+          "application/json",
+          "application/edn",
+          "application/transit+json",
+          "application/transit+msgpack",
+          "application/transit"
+        ],
+        "consumes": [
+          "application/json",
+          "application/edn",
+          "application/x-www-form-urlencoded",
+          "application/transit+json",
+          "application/transit+msgpack"
+        ],
+        "tags": [
+          "pets"
+        ],
+        "summary": "Get a pet by id",
+        "parameters": [
+          {
+            "in": "path",
+            "name": "id",
+            "description": "",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          }
+        ],
+        "operationId": "get-pet"
+      },
+      "put": {
+        "responses": {
+          "400": {
+            "description": "The request contains bad syntax or cannot be fulfilled."
+          },
+          "500": {
+            "description": "There was an internal server error."
+          },
+          "404": {
+            "schema": {
+              "type": "string"
+            },
+            "description": "The requested resource could not be found but may be available again in the future."
+          },
+          "200": {
+            "schema": {
+              "type": "string"
+            },
+            "description": "OK"
+          }
+        },
+        "produces": [
+          "application/json",
+          "application/edn",
+          "application/transit+json",
+          "application/transit+msgpack",
+          "application/transit"
+        ],
+        "consumes": [
+          "application/json",
+          "application/edn",
+          "application/x-www-form-urlencoded",
+          "application/transit+json",
+          "application/transit+msgpack"
+        ],
+        "tags": [
+          "pets"
+        ],
+        "summary": "Update a pet",
+        "parameters": [
+          {
+            "in": "path",
+            "name": "id",
+            "description": "",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          },
+          {
+            "in": "body",
+            "name": "Pet",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/Pet"
+            }
+          }
+        ],
+        "operationId": "update-pet"
+      },
+      "delete": {
+        "responses": {
+          "400": {
+            "description": "The request contains bad syntax or cannot be fulfilled."
+          },
+          "500": {
+            "description": "There was an internal server error."
+          },
+          "404": {
+            "schema": {
+              "type": "string"
+            },
+            "description": "The requested resource could not be found but may be available again in the future."
+          },
+          "200": {
+            "schema": {
+              "type": "string"
+            },
+            "description": "OK"
+          }
+        },
+        "produces": [
+          "application/json",
+          "application/edn",
+          "application/transit+json",
+          "application/transit+msgpack",
+          "application/transit"
+        ],
+        "consumes": [
+          "application/json",
+          "application/edn",
+          "application/x-www-form-urlencoded",
+          "application/transit+json",
+          "application/transit+msgpack"
+        ],
+        "tags": [
+          "pets"
+        ],
+        "summary": "Delete a pet by id",
+        "parameters": [
+          {
+            "in": "path",
+            "name": "id",
+            "description": "",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          }
+        ],
+        "operationId": "delete-pet"
+      }
+    }
+  },
+  "tags": [
+    {
+      "name": "pets",
+      "description": "Everything about your Pets",
+      "externalDocs": {
+        "description": "Find out more",
+        "url": "http://swagger.io"
+      }
+    },
+    {
+      "name": "orders",
+      "description": "Operations about orders"
+    }
+  ],
+  "definitions": {
+    "Pet": {
+      "type": "object",
+      "properties": {
+        "name": {
+          "type": "string"
+        },
+        "type": {
+          "type": "string"
+        },
+        "age": {
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name",
+        "type",
+        "age"
+      ]
+    },
+    "PetWithId": {
+      "type": "object",
+      "properties": {
+        "name": {
+          "type": "string"
+        },
+        "type": {
+          "type": "string"
+        },
+        "age": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "id": {
+          "type": "string",
+          "format": "uuid"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name",
+        "type",
+        "age"
+      ]
+    },
+    "Response39145": {
+      "type": "object",
+      "properties": {
+        "pets": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/PetWithId"
+          }
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "pets"
+      ]
+    },
+    "Response39146": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string",
+          "format": "uuid"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id"
+      ]
+    }
+  }
+}
diff --git a/core/test/martian/interceptors_test.cljc b/core/test/martian/interceptors_test.cljc
index 65bbf1c..067e1f8 100644
--- a/core/test/martian/interceptors_test.cljc
+++ b/core/test/martian/interceptors_test.cljc
@@ -134,3 +134,17 @@
         (is (= {:body encoded-body
                 :headers {:content-type "text/magical+json"}}
                (:response result)))))))
+
+(deftest supported-content-types-test
+  (testing "picks up the supported content-types from the encoding/decoding interceptors"
+    (let [encode-body i/default-encode-body
+          coerce-response (i/coerce-response (assoc (encoders/default-encoders)
+                                                    "text/magical+json" {:encode encoders/json-encode
+                                                                         :decode #(encoders/json-decode % keyword)
+                                                                         :as :magic}))]
+
+      (is (= {:encodes #?(:clj #{"application/json" "application/transit+msgpack" "application/transit+json" "application/edn"}
+                          :cljs #{"application/json" "application/transit+json" "application/edn"})
+              :decodes #?(:clj #{"application/json" "text/magical+json" "application/transit+msgpack" "application/transit+json" "application/edn"}
+                          :cljs #{"application/json" "text/magical+json" "application/transit+json" "application/edn"})}
+             (i/supported-content-types [encode-body coerce-response]))))))
diff --git a/core/test/martian/openapi_test.cljc b/core/test/martian/openapi_test.cljc
index 72928c9..e0a77b0 100644
--- a/core/test/martian/openapi_test.cljc
+++ b/core/test/martian/openapi_test.cljc
@@ -1,48 +1,62 @@
 (ns martian.openapi-test
   (:require [clojure.java.io :as io]
-            [clojure.test :refer [deftest is]]
+            [clojure.test :refer [deftest is testing]]
             [cheshire.core :refer [parse-string]]
             [schema.core :as s]
             [martian.openapi :refer [openapi->handlers]]))
 
 (deftest openapi-sanity-check
-  (is (= (-> (parse-string (slurp (io/resource "openapi.json")))
-             (openapi->handlers ["application/json" "application/octet-stream"])
-             (->> (filter #(= (:route-name %) :update-pet)))
-             first
-             (dissoc :openapi-definition))
-         {:summary        "Update an existing pet"
-          :description    "Update an existing pet by Id"
-          :method         :put
-          :produces       ["application/json"]
-          :path-schema    {}
-          :query-schema   {}
-          :form-schema    {}
-          :path-parts     ["/pet"]
-          :headers-schema {}
-          :consumes       ["application/json"]
-          :body-schema
-          {:body
-           {(s/optional-key :id)       s/Int
-            :name                      s/Str
-            (s/optional-key :category) {(s/optional-key :id)   s/Int
-                                        (s/optional-key :name) s/Str}
-            :photoUrls                 [s/Str]
-            (s/optional-key :tags)     [{(s/optional-key :id)   s/Int
-                                         (s/optional-key :name) s/Str}]
-            (s/optional-key :status)   (s/enum "sold" "pending" "available")}}
-          :route-name     :update-pet
-          :response-schemas
-          [{:status (s/eq 200)
-            :body
-            {(s/optional-key :id)       s/Int
-             :name                      s/Str
-             (s/optional-key :category) {(s/optional-key :id)   s/Int
-                                         (s/optional-key :name) s/Str}
-             :photoUrls                 [s/Str]
-             (s/optional-key :tags)     [{(s/optional-key :id)   s/Int
-                                          (s/optional-key :name) s/Str}]
-             (s/optional-key :status)   (s/enum "sold" "pending" "available")}}
-           {:status (s/eq 400) :body nil}
-           {:status (s/eq 404) :body nil}
-           {:status (s/eq 405) :body nil}]})))
+  (testing "parses each handler"
+    (is (= {:summary        "Update an existing pet"
+            :description    "Update an existing pet by Id"
+            :method         :put
+            :produces       ["application/json"]
+            :path-schema    {}
+            :query-schema   {}
+            :form-schema    {}
+            :path-parts     ["/pet"]
+            :headers-schema {}
+            :consumes       ["application/json"]
+            :body-schema
+            {:body
+             {(s/optional-key :id)       s/Int
+              :name                      s/Str
+              (s/optional-key :category) {(s/optional-key :id)   s/Int
+                                          (s/optional-key :name) s/Str}
+              :photoUrls                 [s/Str]
+              (s/optional-key :tags)     [{(s/optional-key :id)   s/Int
+                                           (s/optional-key :name) s/Str}]
+              (s/optional-key :status)   (s/enum "sold" "pending" "available")}}
+            :route-name     :update-pet
+            :response-schemas
+            [{:status (s/eq 200)
+              :body
+              {(s/optional-key :id)       s/Int
+               :name                      s/Str
+               (s/optional-key :category) {(s/optional-key :id)   s/Int
+                                           (s/optional-key :name) s/Str}
+               :photoUrls                 [s/Str]
+               (s/optional-key :tags)     [{(s/optional-key :id)   s/Int
+                                            (s/optional-key :name) s/Str}]
+               (s/optional-key :status)   (s/enum "sold" "pending" "available")}}
+             {:status (s/eq 400) :body nil}
+             {:status (s/eq 404) :body nil}
+             {:status (s/eq 405) :body nil}]}
+
+           (-> (parse-string (slurp (io/resource "openapi.json")))
+               (openapi->handlers {:encodes ["application/json" "application/octet-stream"]
+                                   :decodes ["application/json" "application/octet-stream"]})
+               (->> (filter #(= (:route-name %) :update-pet)))
+               first
+               (dissoc :openapi-definition)))))
+
+  (testing "chooses the first supported content-type"
+    (is (= {:consumes ["application/xml"]
+            :produces ["application/json"]}
+
+           (-> (parse-string (slurp (io/resource "openapi.json")))
+               (openapi->handlers {:encodes ["application/xml"]
+                                   :decodes ["application/json"]})
+               (->> (filter #(= (:route-name %) :update-pet)))
+               first
+               (select-keys [:consumes :produces]))))))
diff --git a/httpkit/test/martian/httpkit_test.clj b/httpkit/test/martian/httpkit_test.clj
index 9feac08..f74d902 100644
--- a/httpkit/test/martian/httpkit_test.clj
+++ b/httpkit/test/martian/httpkit_test.clj
@@ -1,17 +1,14 @@
 (ns martian.httpkit-test
   (:require [martian.httpkit :as martian-http]
             [martian.core :as martian]
-            [martian.interceptors :as interceptors]
-            [martian.server-stub :refer [with-server swagger-url]]
+            [martian.server-stub :refer [with-server swagger-url openapi-url]]
             [martian.encoders :as encoders]
             [martian.test-utils :refer [input-stream->byte-array]]
-            [clojure.test :refer :all]
-            [clojure.java.io :as io]
-            [cheshire.core :as json]))
+            [clojure.test :refer [deftest testing is use-fixtures]]))
 
 (use-fixtures :once with-server)
 
-(deftest http-test
+(deftest swagger-http-test
   (let [m (martian-http/bootstrap-swagger swagger-url)]
 
     (testing "default encoders"
@@ -38,3 +35,12 @@
               :type "Dog"
               :age 3}
              (:body response))))))
+
+(deftest openapi-bootstrap-test
+  (let [m (martian-http/bootstrap-openapi openapi-url)]
+
+    (is (= "http://localhost:8888/openapi/v3"
+           (:api-root m)))
+
+    (is (contains? (set (map first (martian/explore m)))
+                   :get-order-by-id))))
diff --git a/test-common/martian/server_stub.clj b/test-common/martian/server_stub.clj
index 52e1517..d72406c 100644
--- a/test-common/martian/server_stub.clj
+++ b/test-common/martian/server_stub.clj
@@ -1,7 +1,5 @@
 (ns martian.server-stub
   (:require [io.pedestal.http :as bootstrap]
-            [io.pedestal.interceptor.chain :refer [terminate]]
-            [io.pedestal.interceptor :refer [interceptor]]
             [pedestal-api
              [core :as api]
              [helpers :refer [before defbefore defhandler handler]]]
@@ -65,6 +63,7 @@
                                 :allowed-origins (constantly true)}})
 
 (def swagger-url (format "http://localhost:%s/swagger.json" (::bootstrap/port service)))
+(def openapi-url (format "http://localhost:%s/openapi.json" (::bootstrap/port service)))
 
 (def with-server
   (fn [f]
diff --git a/test-common/public/openapi.json b/test-common/public/openapi.json
new file mode 100644
index 0000000..f1a427a
--- /dev/null
+++ b/test-common/public/openapi.json
@@ -0,0 +1,1094 @@
+{
+    "openapi": "3.0.2",
+    "info": {
+        "title": "Swagger Petstore - OpenAPI 3.0",
+        "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification.  You can find out more about\nSwagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)",
+        "termsOfService": "http://swagger.io/terms/",
+        "contact": {
+            "email": "apiteam@swagger.io"
+        },
+        "license": {
+            "name": "Apache 2.0",
+            "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+        },
+        "version": "1.0.5"
+    },
+    "externalDocs": {
+        "description": "Find out more about Swagger",
+        "url": "http://swagger.io"
+    },
+    "servers": [{
+        "url": "/openapi/v3"
+    }],
+    "tags": [{
+        "name": "pet",
+        "description": "Everything about your Pets",
+        "externalDocs": {
+            "description": "Find out more",
+            "url": "http://swagger.io"
+        }
+    }, {
+        "name": "store",
+        "description": "Operations about user"
+    }, {
+        "name": "user",
+        "description": "Access to Petstore orders",
+        "externalDocs": {
+            "description": "Find out more about our store",
+            "url": "http://swagger.io"
+        }
+    }],
+    "paths": {
+        "/pet": {
+            "put": {
+                "tags": ["pet"],
+                "summary": "Update an existing pet",
+                "description": "Update an existing pet by Id",
+                "operationId": "updatePet",
+                "requestBody": {
+                    "description": "Update an existent pet in the store",
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Pet"
+                            }
+                        },
+                        "application/xml": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Pet"
+                            }
+                        },
+                        "application/x-www-form-urlencoded": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Pet"
+                            }
+                        }
+                    },
+                    "required": true
+                },
+                "responses": {
+                    "200": {
+                        "description": "Successful operation",
+                        "content": {
+                            "application/xml": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Pet"
+                                }
+                            },
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Pet"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid ID supplied"
+                    },
+                    "404": {
+                        "description": "Pet not found"
+                    },
+                    "405": {
+                        "description": "Validation exception"
+                    }
+                },
+                "security": [{
+                    "petstore_auth": ["write:pets", "read:pets"]
+                }]
+            },
+            "post": {
+                "tags": ["pet"],
+                "summary": "Add a new pet to the store",
+                "description": "Add a new pet to the store",
+                "operationId": "addPet",
+                "requestBody": {
+                    "description": "Create a new pet in the store",
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Pet"
+                            }
+                        },
+                        "application/xml": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Pet"
+                            }
+                        },
+                        "application/x-www-form-urlencoded": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Pet"
+                            }
+                        }
+                    },
+                    "required": true
+                },
+                "responses": {
+                    "200": {
+                        "description": "Successful operation",
+                        "content": {
+                            "application/xml": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Pet"
+                                }
+                            },
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Pet"
+                                }
+                            }
+                        }
+                    },
+                    "405": {
+                        "description": "Invalid input"
+                    }
+                },
+                "security": [{
+                    "petstore_auth": ["write:pets", "read:pets"]
+                }]
+            }
+        },
+        "/pet/findByStatus": {
+            "get": {
+                "tags": ["pet"],
+                "summary": "Finds Pets by status",
+                "description": "Multiple status values can be provided with comma separated strings",
+                "operationId": "findPetsByStatus",
+                "parameters": [{
+                    "name": "status",
+                    "in": "query",
+                    "description": "Status values that need to be considered for filter",
+                    "required": false,
+                    "explode": true,
+                    "schema": {
+                        "type": "string",
+                        "default": "available",
+                        "enum": ["available", "pending", "sold"]
+                    }
+                }],
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/xml": {
+                                "schema": {
+                                    "type": "array",
+                                    "items": {
+                                        "$ref": "#/components/schemas/Pet"
+                                    }
+                                }
+                            },
+                            "application/json": {
+                                "schema": {
+                                    "type": "array",
+                                    "items": {
+                                        "$ref": "#/components/schemas/Pet"
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid status value"
+                    }
+                },
+                "security": [{
+                    "petstore_auth": ["write:pets", "read:pets"]
+                }]
+            }
+        },
+        "/pet/findByTags": {
+            "get": {
+                "tags": ["pet"],
+                "summary": "Finds Pets by tags",
+                "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.",
+                "operationId": "findPetsByTags",
+                "parameters": [{
+                    "name": "tags",
+                    "in": "query",
+                    "description": "Tags to filter by",
+                    "required": false,
+                    "explode": true,
+                    "schema": {
+                        "type": "array",
+                        "items": {
+                            "type": "string"
+                        }
+                    }
+                }],
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/xml": {
+                                "schema": {
+                                    "type": "array",
+                                    "items": {
+                                        "$ref": "#/components/schemas/Pet"
+                                    }
+                                }
+                            },
+                            "application/json": {
+                                "schema": {
+                                    "type": "array",
+                                    "items": {
+                                        "$ref": "#/components/schemas/Pet"
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid tag value"
+                    }
+                },
+                "security": [{
+                    "petstore_auth": ["write:pets", "read:pets"]
+                }]
+            }
+        },
+        "/pet/{petId}": {
+            "get": {
+                "tags": ["pet"],
+                "summary": "Find pet by ID",
+                "description": "Returns a single pet",
+                "operationId": "getPetById",
+                "parameters": [{
+                    "name": "petId",
+                    "in": "path",
+                    "description": "ID of pet to return",
+                    "required": true,
+                    "schema": {
+                        "type": "integer",
+                        "format": "int64"
+                    }
+                }],
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/xml": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Pet"
+                                }
+                            },
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Pet"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid ID supplied"
+                    },
+                    "404": {
+                        "description": "Pet not found"
+                    }
+                },
+                "security": [{
+                    "api_key": []
+                }, {
+                    "petstore_auth": ["write:pets", "read:pets"]
+                }]
+            },
+            "post": {
+                "tags": ["pet"],
+                "summary": "Updates a pet in the store with form data",
+                "description": "",
+                "operationId": "updatePetWithForm",
+                "parameters": [{
+                    "name": "petId",
+                    "in": "path",
+                    "description": "ID of pet that needs to be updated",
+                    "required": true,
+                    "schema": {
+                        "type": "integer",
+                        "format": "int64"
+                    }
+                }, {
+                    "name": "name",
+                    "in": "query",
+                    "description": "Name of pet that needs to be updated",
+                    "schema": {
+                        "type": "string"
+                    }
+                }, {
+                    "name": "status",
+                    "in": "query",
+                    "description": "Status of pet that needs to be updated",
+                    "schema": {
+                        "type": "string"
+                    }
+                }],
+                "responses": {
+                    "405": {
+                        "description": "Invalid input"
+                    }
+                },
+                "security": [{
+                    "petstore_auth": ["write:pets", "read:pets"]
+                }]
+            },
+            "delete": {
+                "tags": ["pet"],
+                "summary": "Deletes a pet",
+                "description": "",
+                "operationId": "deletePet",
+                "parameters": [{
+                    "name": "api_key",
+                    "in": "header",
+                    "description": "",
+                    "required": false,
+                    "schema": {
+                        "type": "string"
+                    }
+                }, {
+                    "name": "petId",
+                    "in": "path",
+                    "description": "Pet id to delete",
+                    "required": true,
+                    "schema": {
+                        "type": "integer",
+                        "format": "int64"
+                    }
+                }],
+                "responses": {
+                    "400": {
+                        "description": "Invalid pet value"
+                    }
+                },
+                "security": [{
+                    "petstore_auth": ["write:pets", "read:pets"]
+                }]
+            }
+        },
+        "/pet/{petId}/uploadImage": {
+            "post": {
+                "tags": ["pet"],
+                "summary": "uploads an image",
+                "description": "",
+                "operationId": "uploadFile",
+                "parameters": [{
+                    "name": "petId",
+                    "in": "path",
+                    "description": "ID of pet to update",
+                    "required": true,
+                    "schema": {
+                        "type": "integer",
+                        "format": "int64"
+                    }
+                }, {
+                    "name": "additionalMetadata",
+                    "in": "query",
+                    "description": "Additional Metadata",
+                    "required": false,
+                    "schema": {
+                        "type": "string"
+                    }
+                }],
+                "requestBody": {
+                    "content": {
+                        "application/octet-stream": {
+                            "schema": {
+                                "type": "string",
+                                "format": "binary"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/ApiResponse"
+                                }
+                            }
+                        }
+                    }
+                },
+                "security": [{
+                    "petstore_auth": ["write:pets", "read:pets"]
+                }]
+            }
+        },
+        "/store/inventory": {
+            "get": {
+                "tags": ["store"],
+                "summary": "Returns pet inventories by status",
+                "description": "Returns a map of status codes to quantities",
+                "operationId": "getInventory",
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "type": "object",
+                                    "additionalProperties": {
+                                        "type": "integer",
+                                        "format": "int32"
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                "security": [{
+                    "api_key": []
+                }]
+            }
+        },
+        "/store/order": {
+            "post": {
+                "tags": ["store"],
+                "summary": "Place an order for a pet",
+                "description": "Place a new order in the store",
+                "operationId": "placeOrder",
+                "requestBody": {
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Order"
+                            }
+                        },
+                        "application/xml": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Order"
+                            }
+                        },
+                        "application/x-www-form-urlencoded": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Order"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Order"
+                                }
+                            }
+                        }
+                    },
+                    "405": {
+                        "description": "Invalid input"
+                    }
+                }
+            }
+        },
+        "/store/order/{orderId}": {
+            "get": {
+                "tags": ["store"],
+                "summary": "Find purchase order by ID",
+                "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions",
+                "operationId": "getOrderById",
+                "parameters": [{
+                    "name": "orderId",
+                    "in": "path",
+                    "description": "ID of order that needs to be fetched",
+                    "required": true,
+                    "schema": {
+                        "type": "integer",
+                        "format": "int64"
+                    }
+                }],
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/xml": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Order"
+                                }
+                            },
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Order"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid ID supplied"
+                    },
+                    "404": {
+                        "description": "Order not found"
+                    }
+                }
+            },
+            "delete": {
+                "tags": ["store"],
+                "summary": "Delete purchase order by ID",
+                "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors",
+                "operationId": "deleteOrder",
+                "parameters": [{
+                    "name": "orderId",
+                    "in": "path",
+                    "description": "ID of the order that needs to be deleted",
+                    "required": true,
+                    "schema": {
+                        "type": "integer",
+                        "format": "int64"
+                    }
+                }],
+                "responses": {
+                    "400": {
+                        "description": "Invalid ID supplied"
+                    },
+                    "404": {
+                        "description": "Order not found"
+                    }
+                }
+            }
+        },
+        "/user": {
+            "post": {
+                "tags": ["user"],
+                "summary": "Create user",
+                "description": "This can only be done by the logged in user.",
+                "operationId": "createUser",
+                "requestBody": {
+                    "description": "Created user object",
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/User"
+                            }
+                        },
+                        "application/xml": {
+                            "schema": {
+                                "$ref": "#/components/schemas/User"
+                            }
+                        },
+                        "application/x-www-form-urlencoded": {
+                            "schema": {
+                                "$ref": "#/components/schemas/User"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "default": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/User"
+                                }
+                            },
+                            "application/xml": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/User"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/user/createWithList": {
+            "post": {
+                "tags": ["user"],
+                "summary": "Creates list of users with given input array",
+                "description": "Creates list of users with given input array",
+                "operationId": "createUsersWithListInput",
+                "requestBody": {
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "type": "array",
+                                "items": {
+                                    "$ref": "#/components/schemas/User"
+                                }
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "200": {
+                        "description": "Successful operation",
+                        "content": {
+                            "application/xml": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/User"
+                                }
+                            },
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/User"
+                                }
+                            }
+                        }
+                    },
+                    "default": {
+                        "description": "successful operation"
+                    }
+                }
+            }
+        },
+        "/user/login": {
+            "get": {
+                "tags": ["user"],
+                "summary": "Logs user into the system",
+                "description": "",
+                "operationId": "loginUser",
+                "parameters": [{
+                    "name": "username",
+                    "in": "query",
+                    "description": "The user name for login",
+                    "required": false,
+                    "schema": {
+                        "type": "string"
+                    }
+                }, {
+                    "name": "password",
+                    "in": "query",
+                    "description": "The password for login in clear text",
+                    "required": false,
+                    "schema": {
+                        "type": "string"
+                    }
+                }],
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "headers": {
+                            "X-Rate-Limit": {
+                                "description": "calls per hour allowed by the user",
+                                "schema": {
+                                    "type": "integer",
+                                    "format": "int32"
+                                }
+                            },
+                            "X-Expires-After": {
+                                "description": "date in UTC when toekn expires",
+                                "schema": {
+                                    "type": "string",
+                                    "format": "date-time"
+                                }
+                            }
+                        },
+                        "content": {
+                            "application/xml": {
+                                "schema": {
+                                    "type": "string"
+                                }
+                            },
+                            "application/json": {
+                                "schema": {
+                                    "type": "string"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid username/password supplied"
+                    }
+                }
+            }
+        },
+        "/user/logout": {
+            "get": {
+                "tags": ["user"],
+                "summary": "Logs out current logged in user session",
+                "description": "",
+                "operationId": "logoutUser",
+                "parameters": [],
+                "responses": {
+                    "default": {
+                        "description": "successful operation"
+                    }
+                }
+            }
+        },
+        "/user/{username}": {
+            "get": {
+                "tags": ["user"],
+                "summary": "Get user by user name",
+                "description": "",
+                "operationId": "getUserByName",
+                "parameters": [{
+                    "name": "username",
+                    "in": "path",
+                    "description": "The name that needs to be fetched. Use user1 for testing. ",
+                    "required": true,
+                    "schema": {
+                        "type": "string"
+                    }
+                }],
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/xml": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/User"
+                                }
+                            },
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/User"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid username supplied"
+                    },
+                    "404": {
+                        "description": "User not found"
+                    }
+                }
+            },
+            "put": {
+                "tags": ["user"],
+                "summary": "Update user",
+                "description": "This can only be done by the logged in user.",
+                "operationId": "updateUser",
+                "parameters": [{
+                    "name": "username",
+                    "in": "path",
+                    "description": "name that need to be deleted",
+                    "required": true,
+                    "schema": {
+                        "type": "string"
+                    }
+                }],
+                "requestBody": {
+                    "description": "Update an existent user in the store",
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/User"
+                            }
+                        },
+                        "application/xml": {
+                            "schema": {
+                                "$ref": "#/components/schemas/User"
+                            }
+                        },
+                        "application/x-www-form-urlencoded": {
+                            "schema": {
+                                "$ref": "#/components/schemas/User"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "default": {
+                        "description": "successful operation"
+                    }
+                }
+            },
+            "delete": {
+                "tags": ["user"],
+                "summary": "Delete user",
+                "description": "This can only be done by the logged in user.",
+                "operationId": "deleteUser",
+                "parameters": [{
+                    "name": "username",
+                    "in": "path",
+                    "description": "The name that needs to be deleted",
+                    "required": true,
+                    "schema": {
+                        "type": "string"
+                    }
+                }],
+                "responses": {
+                    "400": {
+                        "description": "Invalid username supplied"
+                    },
+                    "404": {
+                        "description": "User not found"
+                    }
+                }
+            }
+        }
+    },
+    "components": {
+        "schemas": {
+            "Order": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64",
+                        "example": 10
+                    },
+                    "petId": {
+                        "type": "integer",
+                        "format": "int64",
+                        "example": 198772
+                    },
+                    "quantity": {
+                        "type": "integer",
+                        "format": "int32",
+                        "example": 7
+                    },
+                    "shipDate": {
+                        "type": "string",
+                        "format": "date-time"
+                    },
+                    "status": {
+                        "type": "string",
+                        "description": "Order Status",
+                        "example": "approved",
+                        "enum": ["placed", "approved", "delivered"]
+                    },
+                    "complete": {
+                        "type": "boolean"
+                    }
+                },
+                "xml": {
+                    "name": "order"
+                }
+            },
+            "Customer": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64",
+                        "example": 100000
+                    },
+                    "username": {
+                        "type": "string",
+                        "example": "fehguy"
+                    },
+                    "address": {
+                        "type": "array",
+                        "xml": {
+                            "name": "addresses",
+                            "wrapped": true
+                        },
+                        "items": {
+                            "$ref": "#/components/schemas/Address"
+                        }
+                    }
+                },
+                "xml": {
+                    "name": "customer"
+                }
+            },
+            "Address": {
+                "type": "object",
+                "properties": {
+                    "street": {
+                        "type": "string",
+                        "example": "437 Lytton"
+                    },
+                    "city": {
+                        "type": "string",
+                        "example": "Palo Alto"
+                    },
+                    "state": {
+                        "type": "string",
+                        "example": "CA"
+                    },
+                    "zip": {
+                        "type": "string",
+                        "example": "94301"
+                    }
+                },
+                "xml": {
+                    "name": "address"
+                }
+            },
+            "Category": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64",
+                        "example": 1
+                    },
+                    "name": {
+                        "type": "string",
+                        "example": "Dogs"
+                    }
+                },
+                "xml": {
+                    "name": "category"
+                }
+            },
+            "User": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64",
+                        "example": 10
+                    },
+                    "username": {
+                        "type": "string",
+                        "example": "theUser"
+                    },
+                    "firstName": {
+                        "type": "string",
+                        "example": "John"
+                    },
+                    "lastName": {
+                        "type": "string",
+                        "example": "James"
+                    },
+                    "email": {
+                        "type": "string",
+                        "example": "john@email.com"
+                    },
+                    "password": {
+                        "type": "string",
+                        "example": "12345"
+                    },
+                    "phone": {
+                        "type": "string",
+                        "example": "12345"
+                    },
+                    "userStatus": {
+                        "type": "integer",
+                        "description": "User Status",
+                        "format": "int32",
+                        "example": 1
+                    }
+                },
+                "xml": {
+                    "name": "user"
+                }
+            },
+            "Tag": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64"
+                    },
+                    "name": {
+                        "type": "string"
+                    }
+                },
+                "xml": {
+                    "name": "tag"
+                }
+            },
+            "Pet": {
+                "required": ["name", "photoUrls"],
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64",
+                        "example": 10
+                    },
+                    "name": {
+                        "type": "string",
+                        "example": "doggie"
+                    },
+                    "category": {
+                        "$ref": "#/components/schemas/Category"
+                    },
+                    "photoUrls": {
+                        "type": "array",
+                        "xml": {
+                            "wrapped": true
+                        },
+                        "items": {
+                            "type": "string",
+                            "xml": {
+                                "name": "photoUrl"
+                            }
+                        }
+                    },
+                    "tags": {
+                        "type": "array",
+                        "xml": {
+                            "wrapped": true
+                        },
+                        "items": {
+                            "$ref": "#/components/schemas/Tag"
+                        }
+                    },
+                    "status": {
+                        "type": "string",
+                        "description": "pet status in the store",
+                        "enum": ["available", "pending", "sold"]
+                    }
+                },
+                "xml": {
+                    "name": "pet"
+                }
+            },
+            "ApiResponse": {
+                "type": "object",
+                "properties": {
+                    "code": {
+                        "type": "integer",
+                        "format": "int32"
+                    },
+                    "type": {
+                        "type": "string"
+                    },
+                    "message": {
+                        "type": "string"
+                    }
+                },
+                "xml": {
+                    "name": "##default"
+                }
+            }
+        },
+        "requestBodies": {
+            "Pet": {
+                "description": "Pet object that needs to be added to the store",
+                "content": {
+                    "application/json": {
+                        "schema": {
+                            "$ref": "#/components/schemas/Pet"
+                        }
+                    },
+                    "application/xml": {
+                        "schema": {
+                            "$ref": "#/components/schemas/Pet"
+                        }
+                    }
+                }
+            },
+            "UserArray": {
+                "description": "List of user object",
+                "content": {
+                    "application/json": {
+                        "schema": {
+                            "type": "array",
+                            "items": {
+                                "$ref": "#/components/schemas/User"
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "securitySchemes": {
+            "petstore_auth": {
+                "type": "oauth2",
+                "flows": {
+                    "implicit": {
+                        "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize",
+                        "scopes": {
+                            "write:pets": "modify pets in your account",
+                            "read:pets": "read your pets"
+                        }
+                    }
+                }
+            },
+            "api_key": {
+                "type": "apiKey",
+                "name": "api_key",
+                "in": "header"
+            }
+        }
+    }
+}