Repository: oliyh/martian
Pull request #132 (2022-01-19 21:45:26)

Add OpenAPI YAML definition support.
For CLJ only.

Addresses #125 

bombaywalla: This PR is ready for review.
oliyh: Hi,

I was looking at the http-kit module which has a require for Cheshire, so it should still have Cheshire in the project clj rather than relying on the user also having a dependency on core (even though they realistically would)
bombaywalla: Sorry if I'm being dense, but...

The `project.clj` for `martian.http-kit` has a dep for `martian.core`, which has a dep for `cheshire`.
So, if a user used `martian.http-kit` (but did not explicitly include `martian.core`) they'd still get `cheshire`, right?
oliyh: Hi @bombaywalla 
Yes, it would still get cheshire on the classpath but transitively. If martian.http-kit only made calls to martian.core, and martian.core made all the calls to cheshire I'd be fine with that, but martian.http-kit explicitly requires cheshire and makes calls to it. To mirror this there should be an explicit dependency on cheshire in martian.http-kit.

This will avoid the scenario where some developer removes all usage of cheshire in martian.core, and removes it from the core project.clj thinking it's no longer used. This would unintentionally and unknowingly break compilation of martian.http-kit.

Many thanks
oliyh: Same for clj-http-lite as well. Hato and clj-http don't need it as those libraries natively convert json to clojure.

Thanks
bombaywalla: Okay.  Makes sense.  Added `cheshire` back to `httpkit` and `clj-http`.
Please review.
oliyh: Hi,

Sorry I made a half mistake in my previous comment - clj-http-lite *does* need cheshire, clj-http doesn't.

Once that's done I think this is good to go, thank you.
bombaywalla: Fixed.

Patch:
diff --git a/clj-http-lite/project.clj b/clj-http-lite/project.clj
index 5bd7cb2..ec9f819 100644
--- a/clj-http-lite/project.clj
+++ b/clj-http-lite/project.clj
@@ -5,8 +5,8 @@
             :url "http://opensource.org/licenses/MIT"}
   :plugins [[lein-modules "0.3.11"]]
   :dependencies [[com.github.oliyh/martian :version]
-                 [org.martinklepsch/clj-http-lite "0.4.3"]
-                 [cheshire "5.10.1"]]
+                 [cheshire "5.10.1"]
+                 [org.martinklepsch/clj-http-lite "0.4.3"]]
   :profiles {:provided {:dependencies [[org.clojure/clojure "1.10.3"]]}
              :dev {:source-paths ["../test-common"]
                    :exclusions [[org.clojure/tools.reader]]
diff --git a/clj-http-lite/src/martian/clj_http_lite.clj b/clj-http-lite/src/martian/clj_http_lite.clj
index d655d3c..171de7c 100644
--- a/clj-http-lite/src/martian/clj_http_lite.clj
+++ b/clj-http-lite/src/martian/clj_http_lite.clj
@@ -1,6 +1,7 @@
 (ns martian.clj-http-lite
   (:require [clj-http.lite.client :as http]
             [cheshire.core :as json]
+            [martian.yaml :as yaml]
             [martian.core :as martian]
             [martian.interceptors :as interceptors]
             [martian.openapi :as openapi]))
@@ -24,7 +25,11 @@
   (martian/bootstrap api-root concise-handlers (merge default-opts opts)))
 
 (defn bootstrap-openapi [url & [{:keys [server-url] :as opts} get-swagger-opts]]
-  (let [definition (-> (http/get url (or get-swagger-opts {})) :body (json/parse-string keyword)) ;; clj-http-lite does not support {:as :json} body conversion (yet) so we do it right here
+  (let [body (:body (http/get url (or get-swagger-opts {})))
+        ;; clj-http-lite does not support {:as :json} body conversion (yet) so we do it right here
+        definition (if (yaml/yaml-url? url)
+                     (yaml/yaml->edn body)
+                     (json/parse-string body keyword))
         base-url (openapi/base-url url server-url definition)]
     (martian/bootstrap-openapi base-url definition (merge default-opts opts))))
 
diff --git a/clj-http/project.clj b/clj-http/project.clj
index f337d4e..6f3dd45 100644
--- a/clj-http/project.clj
+++ b/clj-http/project.clj
@@ -5,8 +5,7 @@
             :url "http://opensource.org/licenses/MIT"}
   :plugins [[lein-modules "0.3.11"]]
   :dependencies [[com.github.oliyh/martian :version]
-                 [clj-http "3.12.3"]
-                 [cheshire "5.10.1"]]
+                 [clj-http "3.12.3"]]
   :profiles {:provided {:dependencies [[org.clojure/clojure "1.10.3"]]}
              :dev {:source-paths ["../test-common"]
                    :exclusions [[org.clojure/tools.reader]]
diff --git a/clj-http/src/martian/clj_http.clj b/clj-http/src/martian/clj_http.clj
index 6198428..dd1422f 100644
--- a/clj-http/src/martian/clj_http.clj
+++ b/clj-http/src/martian/clj_http.clj
@@ -2,8 +2,8 @@
   (:require [clj-http.client :as http]
             [martian.core :as martian]
             [martian.interceptors :as interceptors]
-            [martian.openapi :as openapi])
-  (:import [java.io ByteArrayInputStream]))
+            [martian.openapi :as openapi]
+            [martian.yaml :as yaml]))
 
 (def perform-request
   {:name ::perform-request
@@ -19,7 +19,9 @@
   (martian/bootstrap api-root concise-handlers (merge default-opts opts)))
 
 (defn bootstrap-openapi [url & [{:keys [server-url] :as opts} get-swagger-opts]]
-  (let [definition (:body (http/get url (merge {:as :json} get-swagger-opts)))
+  (let [definition (if (yaml/yaml-url? url)
+                     (yaml/yaml->edn (:body (http/get url (dissoc get-swagger-opts :as))))
+                     (:body (http/get url (merge {:as :json} get-swagger-opts))))
         base-url (openapi/base-url url server-url definition)]
     (martian/bootstrap-openapi base-url definition (merge default-opts opts))))
 
diff --git a/core/project.clj b/core/project.clj
index 0264d1c..4e702ca 100644
--- a/core/project.clj
+++ b/core/project.clj
@@ -11,6 +11,7 @@
                  [org.clojure/spec.alpha "0.2.194"]
                  [camel-snake-kebab "0.4.2"]
                  [cheshire "5.10.1"]
+                 [clj-commons/clj-yaml "0.7.107"]
                  [lambdaisland/uri "1.12.89"]
 
                  [com.cognitect/transit-clj "1.0.324"]
diff --git a/core/src/martian/yaml.clj b/core/src/martian/yaml.clj
new file mode 100644
index 0000000..a26fea4
--- /dev/null
+++ b/core/src/martian/yaml.clj
@@ -0,0 +1,39 @@
+(ns martian.yaml
+  "Handle OpenAPI specs in YAML."
+  (:require [clojure.walk :as w]
+            [clojure.string :as string]
+            [clj-yaml.core :as yaml]))
+
+;;; NOTE: Currently only for CLJ
+
+(defn yaml-url?
+  "Predicate that returns true if the `url` has a YAML extension.
+  False otherwise."
+  [url]
+  (boolean (or (string/ends-with? url ".yaml") (string/ends-with? url ".yml"))))
+
+(defn cleanup
+  "Clean up the EDN returned by clj-commons/clj-yaml
+    to be compatible with what martian expects."
+  [edn]
+  (w/postwalk (fn [x]
+                (cond
+                  ;; replace all LazySeqs with Vectors
+                  ;; See https://github.com/clj-commons/clj-yaml/pull/18
+                  (and (seq? x) (not (vector? x)))
+                  (into [] x)
+                  ;; make sure all the keys of maps are keywords, including keys that were numbers
+                  ;; See https://github.com/clj-commons/clj-yaml/blob/master/src/clojure/clj_yaml/core.clj#L128-L129
+                  (map? x)
+                  (into {} (map (fn [[k v]] [(or (keyword k) (if (number? k) (keyword (str k)) k)) v]) x))
+                  ;; otherwise do nothing
+                  :else
+                  x))
+              edn))
+
+(defn yaml->edn
+  "Convert a YAML OpenAPI Spec string `s` to EDN compatible with martian."
+  [s]
+  (-> s
+      (yaml/parse-string)
+      (cleanup)))
diff --git a/hato/project.clj b/hato/project.clj
index 5aa8897..52e52de 100644
--- a/hato/project.clj
+++ b/hato/project.clj
@@ -5,8 +5,7 @@
             :url "http://opensource.org/licenses/MIT"}
   :plugins [[lein-modules "0.3.11"]]
   :dependencies [[com.github.oliyh/martian :version]
-                 [hato "0.8.2"]
-                 [cheshire "5.10.1"]]
+                 [hato "0.8.2"]]
   :profiles {:provided {:dependencies [[org.clojure/clojure "1.10.3"]]}
              :dev {:source-paths ["../test-common"]
                    :exclusions [[org.clojure/tools.reader]]
diff --git a/hato/src/martian/hato.clj b/hato/src/martian/hato.clj
index 52a6a28..1a582be 100644
--- a/hato/src/martian/hato.clj
+++ b/hato/src/martian/hato.clj
@@ -3,6 +3,7 @@
             [martian.core :as martian]
             [martian.interceptors :as interceptors]
             [martian.openapi :as openapi]
+            [martian.yaml :as yaml]
             [clojure.walk :refer [keywordize-keys stringify-keys]]
             [tripod.context :as tc]))
 
@@ -55,7 +56,9 @@
   (martian/bootstrap api-root concise-handlers (merge default-opts opts)))
 
 (defn bootstrap-openapi [url & [{:keys [server-url] :as opts} get-swagger-opts]]
-  (let [definition (:body (http/get url (merge {:as :json} get-swagger-opts)))
+  (let [definition (if (yaml/yaml-url? url)
+                     (yaml/yaml->edn (:body (http/get url (dissoc get-swagger-opts :as))))
+                     (:body (http/get url (merge {:as :json} get-swagger-opts))))
         base-url (openapi/base-url url server-url definition)]
     (martian/bootstrap-openapi base-url definition (merge default-opts opts))))
 
diff --git a/httpkit/project.clj b/httpkit/project.clj
index 8c73284..9bc589b 100644
--- a/httpkit/project.clj
+++ b/httpkit/project.clj
@@ -5,8 +5,8 @@
             :url "http://opensource.org/licenses/MIT"}
   :plugins [[lein-modules "0.3.11"]]
   :dependencies [[com.github.oliyh/martian :version]
-                 [http-kit "2.5.3"]
-                 [cheshire "5.10.1"]]
+                 [cheshire "5.10.1"]
+                 [http-kit "2.5.3"]]
   :profiles {:provided {:dependencies [[org.clojure/clojure "1.10.3"]]}
              :dev {:source-paths ["../test-common"]
                    :exclusions [[org.clojure/tools.reader]]
diff --git a/httpkit/src/martian/httpkit.clj b/httpkit/src/martian/httpkit.clj
index 214a33f..33e6d29 100644
--- a/httpkit/src/martian/httpkit.clj
+++ b/httpkit/src/martian/httpkit.clj
@@ -3,9 +3,9 @@
             [martian.core :as martian]
             [martian.interceptors :as interceptors]
             [martian.openapi :as openapi]
+            [martian.yaml :as yaml]
             [cheshire.core :as json]
-            [tripod.context :as tc])
-  (:import [java.net URL]))
+            [tripod.context :as tc]))
 
 (def go-async interceptors/remove-stack)
 
@@ -32,7 +32,10 @@
 (defn bootstrap-openapi [url & [{:keys [server-url] :as opts} get-swagger-opts]]
   (let [definition @(http/get url
                               (merge {:as :text} get-swagger-opts)
-                              (fn [{:keys [body]}] (json/decode body keyword)))
+                              (fn [{:keys [body]}]
+                                (if (yaml/yaml-url? url)
+                                  (yaml/yaml->edn body)
+                                  (json/decode body keyword))))
         base-url (openapi/base-url url server-url definition)]
     (martian/bootstrap-openapi base-url definition (merge default-opts opts))))
 


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 8b5f45c..e648f74 100644
--- a/clj-http-lite/test/martian/clj_http_lite_test.clj
+++ b/clj-http-lite/test/martian/clj_http_lite_test.clj
@@ -2,7 +2,7 @@
   (:require [martian.clj-http-lite :as martian-http]
             [martian.core :as martian]
             [martian.encoders :as encoders]
-            [martian.server-stub :refer [with-server swagger-url openapi-url openapi-test-url]]
+            [martian.server-stub :refer [with-server swagger-url openapi-url openapi-test-url openapi-yaml-url openapi-test-yaml-url]]
             [martian.test-utils :refer [input-stream->byte-array]]
             [clojure.test :refer [deftest testing is use-fixtures]]))
 
@@ -36,7 +36,12 @@
              (:body response))))))
 
 (deftest openapi-bootstrap-test
-  (let [m (martian-http/bootstrap-openapi openapi-url)]
+  (let [m (martian-http/bootstrap-openapi openapi-url)
+        myaml (martian-http/bootstrap-openapi openapi-yaml-url)]
+
+    (is (= "https://sandbox.example.com"
+           (:api-root (martian-http/bootstrap-openapi openapi-test-yaml-url)))
+        "check yaml description")
 
     (is (= "https://sandbox.example.com"
            (:api-root (martian-http/bootstrap-openapi openapi-test-url)))
@@ -50,6 +55,12 @@
            (:api-root (martian-http/bootstrap-openapi openapi-test-url {:server-url "/v3.1"})))
         "check relative server url via opts")
 
+    (is (= "http://localhost:8888/openapi/v3"
+           (:api-root myaml)))
+
+    (is (contains? (set (map first (martian/explore myaml)))
+                   :get-order-by-id))
+
     (is (= "http://localhost:8888/openapi/v3"
            (:api-root m)))
 
diff --git a/clj-http/test/martian/clj_http_test.clj b/clj-http/test/martian/clj_http_test.clj
index 9bbeddf..18632bf 100644
--- a/clj-http/test/martian/clj_http_test.clj
+++ b/clj-http/test/martian/clj_http_test.clj
@@ -2,7 +2,7 @@
   (:require [martian.clj-http :as martian-http]
             [martian.core :as martian]
             [martian.encoders :as encoders]
-            [martian.server-stub :refer [with-server swagger-url openapi-url openapi-test-url]]
+            [martian.server-stub :refer [with-server swagger-url openapi-url openapi-test-url openapi-yaml-url openapi-test-yaml-url]]
             [martian.test-utils :refer [input-stream->byte-array]]
             [clojure.test :refer [deftest testing is use-fixtures]]))
 
@@ -37,7 +37,12 @@
              (:body response))))))
 
 (deftest openapi-bootstrap-test
-  (let [m (martian-http/bootstrap-openapi openapi-url)]
+  (let [m (martian-http/bootstrap-openapi openapi-url)
+        myaml (martian-http/bootstrap-openapi openapi-yaml-url)]
+
+    (is (= "https://sandbox.example.com"
+           (:api-root (martian-http/bootstrap-openapi openapi-test-yaml-url)))
+        "check yaml description")
 
     (is (= "https://sandbox.example.com"
            (:api-root (martian-http/bootstrap-openapi openapi-test-url)))
@@ -51,6 +56,12 @@
            (:api-root (martian-http/bootstrap-openapi openapi-test-url {:server-url "/v3.1"})))
         "check relative server url via opts")
 
+    (is (= "http://localhost:8888/openapi/v3"
+           (:api-root myaml)))
+
+    (is (contains? (set (map first (martian/explore myaml)))
+                   :get-order-by-id))
+
     (is (= "http://localhost:8888/openapi/v3"
            (:api-root m)))
 
diff --git a/core/test-resources/openapi.yaml b/core/test-resources/openapi.yaml
new file mode 100644
index 0000000..2f1392b
--- /dev/null
+++ b/core/test-resources/openapi.yaml
@@ -0,0 +1,804 @@
+---
+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
+    Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!
+    You can now help us improve the API whether it's by making changes to the definition itself or to the code.
+    That way, with time, we can improve the API in general, and expose some of the new features in OAS3.
+
+    Some useful links:
+    - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)
+    - [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
diff --git a/core/test/martian/test_helpers.cljc b/core/test/martian/test_helpers.cljc
index cb4710f..c462d3e 100644
--- a/core/test/martian/test_helpers.cljc
+++ b/core/test/martian/test_helpers.cljc
@@ -1,7 +1,12 @@
 (ns martian.test-helpers
   #?(:clj (:require [clojure.java.io :as io]
+                    [clj-yaml.core :as yaml]
                     [cheshire.core :as json])))
 
 #?(:clj
    (defmacro json-resource [resource-name]
      (json/parse-string (slurp (io/resource resource-name)))))
+
+#?(:clj
+   (defmacro yaml-resource [resource-name]
+     (yaml/parse-string (slurp (io/resource resource-name)))))
diff --git a/core/test/martian/yaml_test.clj b/core/test/martian/yaml_test.clj
new file mode 100644
index 0000000..1e57f69
--- /dev/null
+++ b/core/test/martian/yaml_test.clj
@@ -0,0 +1,67 @@
+(ns martian.yaml-test
+  (:require [martian.test-helpers :refer [yaml-resource]]
+            [clojure.test :refer [deftest is testing]]
+            [schema.core :as s]
+            [martian.yaml :as yaml]
+            [martian.openapi :refer [openapi->handlers]]))
+
+(def openapi-yaml
+  (yaml-resource "openapi.yaml"))
+
+(deftest openapi-sanity-check
+  (testing "parses each handler"
+    (is (= {:summary        "Update an existing pet"
+            :description    "Update an existing pet by Id"
+            :method         :put
+            :produces       ["application/json"]
+            :path-schema    nil
+            :query-schema   nil
+            :form-schema    nil
+            :path-parts     ["/pet"]
+            :headers-schema nil
+            :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}]}
+
+           (-> openapi-yaml
+               (yaml/cleanup)
+               (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"]}
+
+           (-> openapi-yaml
+               (yaml/cleanup)
+               (openapi->handlers {:encodes ["application/xml"]
+                                   :decodes ["application/json"]})
+               (->> (filter #(= (:route-name %) :update-pet)))
+               first
+               (select-keys [:consumes :produces]))))))
diff --git a/hato/test/martian/hato_test.clj b/hato/test/martian/hato_test.clj
index 6242b99..34bba10 100644
--- a/hato/test/martian/hato_test.clj
+++ b/hato/test/martian/hato_test.clj
@@ -2,7 +2,7 @@
   (:require [martian.hato :as martian-http]
             [martian.core :as martian]
             [martian.encoders :as encoders]
-            [martian.server-stub :refer [with-server swagger-url openapi-url openapi-test-url]]
+            [martian.server-stub :refer [with-server swagger-url openapi-url openapi-test-url openapi-yaml-url openapi-test-yaml-url]]
             [martian.test-utils :refer [input-stream->byte-array]]
             [clojure.test :refer [deftest testing is use-fixtures]]))
 
@@ -91,7 +91,12 @@
                @(martian/response-for m :get-pet {:id -1})))))))
 
 (deftest openapi-bootstrap-test
-  (let [m (martian-http/bootstrap-openapi openapi-url)]
+  (let [m (martian-http/bootstrap-openapi openapi-url)
+        myaml (martian-http/bootstrap-openapi openapi-yaml-url)]
+
+    (is (= "https://sandbox.example.com"
+           (:api-root (martian-http/bootstrap-openapi openapi-test-yaml-url)))
+        "check yaml description")
 
     (is (= "https://sandbox.example.com"
            (:api-root (martian-http/bootstrap-openapi openapi-test-url)))
@@ -105,6 +110,13 @@
            (:api-root (martian-http/bootstrap-openapi openapi-test-url {:server-url "/v3.1"})))
         "check relative server url via opts")
 
+    (is (= "http://localhost:8888/openapi/v3"
+           (:api-root myaml))
+        "check yaml description")
+
+    (is (contains? (set (map first (martian/explore myaml)))
+                   :get-order-by-id))
+
     (is (= "http://localhost:8888/openapi/v3"
            (:api-root m)))
 
diff --git a/httpkit/test/martian/httpkit_test.clj b/httpkit/test/martian/httpkit_test.clj
index b09c605..58c5952 100644
--- a/httpkit/test/martian/httpkit_test.clj
+++ b/httpkit/test/martian/httpkit_test.clj
@@ -1,7 +1,7 @@
 (ns martian.httpkit-test
   (:require [martian.httpkit :as martian-http]
             [martian.core :as martian]
-            [martian.server-stub :refer [with-server swagger-url openapi-url openapi-test-url]]
+            [martian.server-stub :refer [with-server swagger-url openapi-url openapi-test-url openapi-yaml-url openapi-test-yaml-url]]
             [martian.encoders :as encoders]
             [martian.test-utils :refer [input-stream->byte-array]]
             [clojure.test :refer [deftest testing is use-fixtures]]))
@@ -37,7 +37,12 @@
              (:body response))))))
 
 (deftest openapi-bootstrap-test
-  (let [m (martian-http/bootstrap-openapi openapi-url)]
+  (let [m (martian-http/bootstrap-openapi openapi-url)
+        myaml (martian-http/bootstrap-openapi openapi-yaml-url)]
+
+    (is (= "https://sandbox.example.com"
+           (:api-root (martian-http/bootstrap-openapi openapi-test-yaml-url)))
+        "check yaml description")
 
     (is (= "https://sandbox.example.com"
            (:api-root (martian-http/bootstrap-openapi openapi-test-url)))
@@ -51,6 +56,12 @@
            (:api-root (martian-http/bootstrap-openapi openapi-test-url {:server-url "/v3.1"})))
         "check relative server url via opts")
 
+    (is (= "http://localhost:8888/openapi/v3"
+           (:api-root myaml)))
+
+    (is (contains? (set (map first (martian/explore myaml)))
+                   :get-order-by-id))
+
     (is (= "http://localhost:8888/openapi/v3"
            (:api-root m)))
 
diff --git a/test-common/martian/server_stub.clj b/test-common/martian/server_stub.clj
index 0f2d232..2a35c17 100644
--- a/test-common/martian/server_stub.clj
+++ b/test-common/martian/server_stub.clj
@@ -64,7 +64,9 @@
 
 (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 openapi-yaml-url (format "http://localhost:%s/openapi.yaml" (::bootstrap/port service)))
 (def openapi-test-url (format "http://localhost:%s/openapi-test.json" (::bootstrap/port service)))
+(def openapi-test-yaml-url (format "http://localhost:%s/openapi-test.yaml" (::bootstrap/port service)))
 
 (def with-server
   (fn [f]
diff --git a/test-common/public/openapi-test.yaml b/test-common/public/openapi-test.yaml
new file mode 100644
index 0000000..f5867d3
--- /dev/null
+++ b/test-common/public/openapi-test.yaml
@@ -0,0 +1,31 @@
+"openapi": "3.1.0"
+"info":
+  "version": "1.2.3"
+  "title": "Repro"
+  "description": "A OAS 3.1.0 definition for testing."
+"paths":
+  "/items":
+    "get":
+      "summary": "Gets a list of items."
+      "description": "Filler"
+      "operationId": "list-items"
+      "parameters":
+        - "in": "query"
+          "name": "ClientId"
+          "description": "Client identifier"
+          "schema":
+            "type": "string"
+          "required": false
+      "responses":
+        "200":
+          "description": "Success."
+          "content":
+            "application/json":
+              "schema":
+                "type": "array"
+                "maxItems": 5000
+                "items":
+                  "type": "string"
+"servers":
+  - "url": "https://sandbox.example.com"
+    "description": "Sandbox"
diff --git a/test-common/public/openapi.yaml b/test-common/public/openapi.yaml
new file mode 100644
index 0000000..2f1392b
--- /dev/null
+++ b/test-common/public/openapi.yaml
@@ -0,0 +1,804 @@
+---
+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
+    Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!
+    You can now help us improve the API whether it's by making changes to the definition itself or to the code.
+    That way, with time, we can improve the API in general, and expose some of the new features in OAS3.
+
+    Some useful links:
+    - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)
+    - [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