metosin / reitit

A fast data-driven routing library for Clojure/Script

Home Page:https://cljdoc.org/d/metosin/reitit/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support for OpenAPI 3 examples

opqdonut opened this issue · comments

OpenAPI3 allows examples in the Media Type object
and Parameter object. There can be multiple named examples. This is in addition to the JSON Schema example field, which only allows one example, but can occur on any level of the schema. See docs for more info.

For full generality, we'd want to support adding examples to the default :request/:response :body, and also to the per-content-type versions of these. I see two different ways to encode examples.

0️⃣ Current syntax

{:decription "examples"
 :parameters {:request {:body [:map [:default :string]]
                        :content {"application/json" [:map [:json :string]]}}}
 :responses {200 {:description "success"
                  :body [:map [:default :string]]
                  :content {"application/json" [:map [:json :string]]}}}}

1️⃣ Examples on the reitit level, next to schema.

Pros:

  • mirrors the openapi structure
  • not easily misused(one more level of nesting

Cons:

  • is this one level of nesting too many?
  • a breaking change to the current syntax
{:description "examples"
 :parameters {:request {:body [:map [:default :string]]
                        :examples {"default-content-type-example" {:summary "an example value"
                                                                   :value {:default "x"}}
                                   "another-example" {:summary "another example"
                                                      :value {:default "another"}}}
                        :content {"application/json" {:body [:map [:json :string]]
                                                      :examples {"json-example" {:summary "an example value"
                                                                                 :value {:json "x"}}}}}}}
 :responses {200 {:description "success"
                  :body [:map [:default :string]]
                  :examples {"default-content-type-example" {:summary "an example value"
                                                             :value {:default "x"}}}
                  :content {"application/json" {:body [:map [:json :string]]
                                                :examples {"json-example" {:summary "an example value"
                                                                           :value {:json "x"}}}}}}}}

2️⃣ Examples in malli (or schema, or spec) props

Pros:

  • less intrusive
  • compatible with current syntax

Cons:

  • easier to misuse: only allowed on the very top level of the schema
{:description "examples"
 :parameters {:request {:body [:map
                               {:openapi/examples {"default-content-type-example" {:summary "an example value"
                                                                                   :value {:default "x"}}
                                                   "another-example" {:summary "another example"
                                                                      :value {:default "another"}}}}
                               [:default :string]]
                        :content {"application/json" [:map
                                                      {:openapi/examples {"json-example" {:summary "an example value"
                                                                                          :value {:json "x"}}}}
                                                      [:json :string]]}}}
 :responses {200 {:description "success"
                  :body [:map
                         {:openapi/examples {"default-content-type-example" {:summary "an example value"
                                                                             :value {:default "x"}}}}
                         [:default :string]]
                  :content {"application/json" [:map
                                                {:openapi/examples {"json-example" {:summary "an example value"
                                                                                          :value {:json "x"}}}}
                                                [:json :string]]}}}}

i would lean towards having the examples on reitit level, not the malli schema level. the reason being that the same malli schema is often used both in the business domain and the web api: i wouldn't want to pollute the business domain with :openapi tags, it should remain as free from interface specifics as possible (for that reason, i was surprised to see :json-schema/example in the malli spec at all).

of course, malli makes it easy to maintain this separation also, and so enriching malli domain representations for different endpoints may work just as well (and is consistent with today's usage, as you state):

;; business domain contains schema definition only
(ns domain)
(def DocumentPath :string)

;; api domain provides endpoint-specific documentation and error handling
(ns api)
(def DocumentPath
  [:and
   {:title "Document Path"
    :description "Document Path"
    :json-schema/example "my-example"
    :error/message "must be a string"} domain/DocumentPath])

Pasting examples from OpenAPI docs:

paths:
  /users:
    post:
      summary: Adds a new user
      requestBody:
        content:
          application/json:     # Media type
            schema:             # Request body contents
              $ref: '#/components/schemas/User'   # Reference to an object
            examples:    # Child of media type
              Jessica:   # Example 1
                value:
                  id: 10
                  name: Jessica Smith
              Ron:       # Example 2
                value:
                  id: 11
                  name: Ron Stewart
      responses:
        '200':
          description: OK

You could already inject the data using :openapi documentation property, right?

{:openapi
 {:requestBody
  {:content
   {"application/json"
    {:examples
     {"Jessica" {:value
                 {:id 10
                  :name "Jessica Smith"}}
      "Ron" {:value
             {:id 11
              :name "Ron Stewart"}}}}}}}}

that is not optimal, but I think it should work. Not super happy with the options we have above, but some compromise on nesting/breaking needs to be done if we want to support this. I propose a huddle @opqdonut to think this through.

Re: using :openapi, I'm pretty sure that won't work because it overrides the generated openapi spec, instead of being deep-merged with it. However some sort of additional mechanism like that might be nice.

Huh, maybe I'm misremembering then. I'll write a test and see if it works after all!

Yep, it works! PR incoming.