Swagger 2.0 implementation for Clojure/Ring using Plumatic Schema (support for clojure.spec via spec-tools).
- Transforms deeply nested Schemas into Swagger JSON Schema definitions
- Extended & symmetric JSON & String serialization & coercion
- Middleware for handling Schemas Validation Errors & Publishing swagger-data
- Local api validator
- Swagger artifact generation
- swagger.json via
ring.swagger.swagger2/swagger-json
- Swagger UI bindings. (get the UI separately as jar or from NPM)
- swagger.json via
The CHANGELOG.
Requires Java 1.8+
- Compojure-Api for Compojure
- fnhouse-swagger for fnhouse
- route-swagger for Pedestal
- yada
- kekkonen
Clojurians slack (join) has a channel #ring-swagger for Ring-swagger related issues. You can also ask questions about Ring-swagger on other channels at Clojurians Slack or at #clojure on Freenode IRC (mention or ring-swagger
to highlight us).
Route definitions are expected as a clojure Map defined by the Schema Contract. The Schema allows mostly any extra keys as ring-swagger tries not to be on your way - one can pass any valid Swagger spec data in.
(require '[ring.swagger.swagger2 :as rs])
(rs/swagger-json {})
; {:swagger "2.0",
; :info {:title "Swagger API", :version "0.0.1"},
; :produces ["application/json"],
; :consumes ["application/json"],
; :paths {},
; :definitions {}}
Info, tags, routes and anonymous nested schemas.
(require '[schema.core :as s])
(s/defschema User {:id s/Str,
:name s/Str
:address {:street s/Str
:city (s/enum :tre :hki)}})
(s/with-fn-validation
(rs/swagger-json
{:info {:version "1.0.0"
:title "Sausages"
:description "Sausage description"
:termsOfService "http://helloreverb.com/terms/"
:contact {:name "My API Team"
:email "[email protected]"
:url "http://www.metosin.fi"}
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}}
:tags [{:name "user"
:description "User stuff"}]
:paths {"/api/ping" {:get {}}
"/user/:id" {:post {:summary "User Api"
:description "User Api description"
:tags ["user"]
:parameters {:path {:id s/Str}
:body User}
:responses {200 {:schema User
:description "Found it!"}
404 {:description "Ohnoes."}}}}}}))
; {:swagger "2.0",
; :info {:title "Sausages",
; :version "1.0.0",
; :description "Sausage description",
; :termsOfService "http://helloreverb.com/terms/",
; :contact {:name "My API Team",
; :email "[email protected]",
; :url "http://www.metosin.fi"},
; :license {:name "Eclipse Public License",
; :url "http://www.eclipse.org/legal/epl-v10.html"}},
; :produces ["application/json"],
; :consumes ["application/json"],
; :tags [{:name "user", :description "User stuff"}],
; :paths {"/api/ping" {:get {:responses {:default {:description ""}}}},
; "/user/{id}" {:post {:summary "User Api",
; :description "User Api description",
; :tags ["user"],
; :parameters [{:in "path",
; :name "id",
; :description "",
; :required true,
; :type "string"}
; {:in "body",
; :name "User",
; :description "",
; :required true,
; :schema {:$ref "#/definitions/User"}}],
; :responses {200 {:schema {:$ref "#/definitions/User"},
; :description "Found it!"},
; 404 {:description "Ohnoes."}}}}},
; :definitions {"User" {:type "object",
; :properties {:id {:type "string"},
; :name {:type "string"},
; :address {:$ref "#/definitions/UserAddress"}},
; :additionalProperties false,
; :required (:id :name :address)},
; "UserAddress" {:type "object",
; :properties {:street {:type "string"},
; :city {:type "string",
; :enum (:tre :hki)}},
; :additionalProperties false,
; :required (:street :city)}}}
producing the following ui:
One can pass extra options-map as a third parameter to swagger-json
. The following options are available:
:ignore-missing-mappings? - (false) boolean whether to silently ignore
missing schema to JSON Schema mappings. if
set to false, IllegalArgumentException is
thrown if a Schema can't be presented as
JSON Schema.
:default-response-description-fn - ((constantly "")) - a fn to generate default
response descriptions from http status code.
Takes a status code (Int) and returns a String.
:handle-duplicate-schemas-fn - (ring.swagger.core/ignore-duplicate-schemas),
a function to handle possible duplicate schema
definitions. Takes schema-name and set of found
attached schema values as parameters. Returns
sequence of schema-name and selected schema value.
:collection-format - Sets the collectionFormat for query and formData
parameters.
Possible values: multi, ssv, csv, tsv, pipes."
For example, to get default response descriptions from the HTTP Spec, you can do the following:
(require '[ring.util.http-status :as status])
(rs/swagger-json
{:paths {"/hello" {:post {:responses {200 nil
425 nil
500 {:description "FAIL"}}}}}}
{:default-response-description-fn status/get-description})
; {:swagger "2.0"
; :info {:title "Swagger API" :version "0.0.1"}
; :consumes ["application/json"]
; :produces ["application/json"]
; :definitions {}
; :paths {"/hello" {:post {:responses {200 {:description "OK"}
; 425 {:description "The collection is unordered."}
; 500 {:description "FAIL"}}}}}}
The generated full spec can be validated against the Swagger JSON Schema with the help of scjsv.
(require '[ring.swagger.validator :as v])
(v/validate (rs/swagger-json {:paths {"/api/ping" {:get nil}}}))
; nil
(v/validate (rs/swagger-json {:pathz {"/api/ping" {:get nil}}}))
; ({:level "error"
; :schema {:loadingURI "#", :pointer ""}
; :instance {:pointer ""}
; :domain "validation"
; :keyword "additionalProperties"
; :message "object instance has properties which are not allowed by the schema: [\"pathz\"]", :unwanted ["pathz"]})
For more information about creating your own adapter, see Collecting API Documentation.
There are the following utility functions for transforming the spec (on the client side):
ring.swagger.swagger2/transform-operations
- transforms the operations under the :paths of a ring-swagger spec
by applying (f operation)
to all operations. If the function returns nil, the given operation is removed.
As an example, one can filter away all operations with :x-no-doc
set to true
:
(defn remove-x-no-doc [endpoint]
(if-not (some-> endpoint :x-no-doc true?)
endpoint))
(transform-operations remove-x-no-doc {:paths {"/a" {:get {:x-no-doc true}, :post {}}
"/b" {:put {:x-no-doc true}}}}))
; {:paths {"/a" {:post {}}}}
Prismatic Schema is used to describe both the input & output schemas for routes.
As Swagger 2.0 Spec Schema is a deterministic subset of JSON Schema, so not all Clojure Schema elements can be used.
There are two possible methods to do this:
- class-based dispatch via
ring.swagger.json-schema/convert-class
. - protocol-based dispatch via
ring.swagger.json-schema/JsonSchema
- theconvert
fn.
Both take the Schema and swagger options map as arguments. Options contain also :in
to denote the possible location
of the schema (nil
, :query
, :header
, :path
, :formData
and :body
).
To support truly symmetric web schemas, one needs also to ensure both JSON Serialization and deserialization/coercion from JSON.
(require '[ring.swagger.json-schema :as json-schema])
(defmethod json-schema/convert-class java.sql.Date [_ _] {:type "string" :format "date"})
(require '[ring.swagger.json-schema :as json-schema])
(extend-type java.util.regex.Pattern
json-schema/JsonSchema
(json-schema/convert [e _]
{:type "string" :pattern (str e)}))
One can also use the options to create more accurate specs (via the :in
option).
(extend-type schema.core.Maybe
json-schema/JsonSchema
(convert [e {:keys [in]}]
(let [schema (->swagger (:schema e))]
(if (#{:query :formData} in)
(assoc schema :allowEmptyValue true)
schema))))
Clojure Schema | JSON Schema | Sample JSON |
---|---|---|
Integer |
integer, int32 | 1 |
Long , s/Int |
integer, int64 | 1 |
Double , Number , s/Num |
number, double | 1.2 |
String , s/Str , Keyword , s/Keyword , Symbol , s/Symbol , s/Any non-body-parameter |
string | "kikka" |
Boolean |
boolean | true |
nil , s/Any body-parameter |
void | |
java.util.regex.Pattern , |
string, regex | [a-z0-9] |
#"[a-z0-9]+" |
string, pattern | "a6" |
s/Uuid , java.util.UUID |
string, uuid | "77e70512-1337-dead-beef-0123456789ab" |
java.util.Date , org.joda.time.DateTime , s/Inst , java.time.Instant |
string, date-time | "2014-02-18T18:25:37.456Z" , also without millis: "2014-02-18T18:25:37Z" |
org.joda.time.LocalDate , java.time.LocalDate |
string, date | "2014-02-19" |
org.joda.time.LocalTime , java.time.LocalTime |
string, time | "16:22" |
(s/enum X Y Z) |
type of X, enum(X,Y,Z) | |
(s/maybe X) |
type of X | |
(s/both X Y Z) |
type of X | |
(s/constrained X pred) |
type of X | |
(s/conditional p1 X p2 Y p3 Z) |
one of type X, Y, Z | |
(s/cond-pre X Y Z) |
one of type X, Y, Z | |
(s/either X Y Z) |
type of X | |
(s/named X name) |
type of X | |
(s/one X name) |
type of X | |
(s/recursive Var) |
Ref to (model) Var | |
(s/eq X) |
type of class of X, enum(X) | |
(s/optional-key X) |
optional key | |
(s/required-key X) |
required key | |
s/Keyword (as a key) |
ignored |
- All supported types have symmetric JSON serialization (Cheshire encoders) & deserialization (Schema coercions)
- Vectors, Sets and Maps can be used as containers
- Maps are presented as Complex Types and References. Model references are resolved automatically.
- Nested maps are transformed automatically into flat maps with generated child references
- Maps can be within valid containers (as only element - heterogeneous schema sequences not supported by the spec)
If Ring-swagger can't transform the Schemas into JSON Schemas, by default a IllegalArgumentException
will be thrown.
Setting the :ignore-missing-mappings?
to true
causes the errors to be ignored - missing schema elements will be
ignored from the generated Swagger schema.
Standard Prismatic Schema names are used. Nested schemas are traversed and all found sub-schemas are named automatically - so that they can be referenced in the generated Swagger spec.
Swagger 2.0 squashes all api models into a single global namespace, so schema name collisions can happen.
When this happens, the function defined by :handle-duplicate-schemas-fn
option is called to resolve the collision.
By default, the collisions are ignored.
One accidental reason for schema name collisions is the use of normal clojure.core
functions to create transformed
copies of the schemas. The normal core functions retain the original schema meta-data and by so the schema name.
(s/defschema User {:id s/Str, :name s/Str})
(def NewUser (dissoc User :id)) ; dissoc does not remove the schema meta-data
(meta User)
; {:name User :ns user}
(meta NewUser)
; {:name User :ns user} <--- fail, now there are two User-schemas around.
There are better schema transformers functions available at schema-tools. It's an implicit dependency of ring-swagger.
Some Schema elements are impossible to accurately describe within boundaries of JSON-Schema or Swagger spec.
You can require ring.swagger.json-schema-dirty
namespace to get JSON Schema dispatching for the following:
WARNING Swagger-UI might not display these correctly and the code generated by swagger-codegen will be inaccurate.
Clojure | JSON Schema | Sample |
---|---|---|
(s/conditional pred X pred Y pred Z) |
x-oneOf: type of X, type of Y, type of Z | |
(s/if pred X Y) |
x-oneOf: type of X, type of Y |
Ring-swagger uses Schema coercions for transforming the input data into vanilla Clojure and back.
There are two coercers in ring.swagger.coerce
, the json-schema-coercion-matcher
and query-schema-coercion-matcher
.
These are enchanced versions of the original Schema coercers, adding support for all the supported Schema elements,
including Dates & Regexps.
In order to allow for custom input coercion, ring-swagger includes a multimethod 'custom-matcher' that can be implemented for custom input types. For example, to coerce currency strings into joda.money.Money objects, you can implement the following:
(require '[ring.swagger.coerce :as coerce])
(import org.joda.money.Money)
(defmethod coerce/custom-matcher org.joda.money.Money [_] #(org.joda.money.Money/parse %))
This will allow org.joda.money.Money objects in your Schema definitions to be coerced correctly. However, this is only for coercing input, see Schema to Swagger JSON Schema conversion for examples on transforming output.
Ring-swagger provides a convenience function for coercion, ring.swagger.schema/coerce!
. It returns either a valid
coerced value of slingshots an Map with type :ring.swagger.schema/validation
. One can catch these exceptions via
ring.swagger.middleware/wrap-validation-errors
and return a JSON-friendly map of the contents.
(require '[schema.core :as s])
(require '[ring.swagger.schema :refer [coerce!]])
(s/defschema Bone {:size Long, :animal (s/enum :cow :tyrannosaurus)})
(coerce! Bone {:size 12, :animal "cow"})
; {:animal :cow, :size 12}
(coerce! Bone {:animal :sheep})
; ExceptionInfo throw+: #schema.utils.ErrorContainer{:error {:animal (not (#{:tyrannosaurus :cow} :sheep)), :size missing-required-key}, :type :ring.swagger.schema/validation} ring.swagger.schema/coerce! (schema.clj:57)
One can add extra meta-data, including descriptions to schema elements using ring.swagger.json-schema/field
and ring.swagger.json-schema/describe
functions. These work by adding meta-data to schema under :json-schema
-key. Objects which don't natively support meta-data, like Java classes, are wrapped automatically into ring.swagger.json-schema/FieldSchema
to enable the meta-data.
(require '[schema.core :as s])
(require '[ring.swagger.schema :as rs])
(require '[ring.swagger.json-schema :as rjs])
(s/defschema Required
(rjs/field
{(s/optional-key :name) s/Str
(s/optional-key :title) s/Str
:address (rjs/field
{:street (rsjs/field s/Str {:description "description here"})}
{:description "Streename"
:example "Ankkalinna 1"})}
{:minProperties 1
:description "I'm required"
:example {:name "Iines"
:title "Ankka"}}))
; produces the following JSON Schema models =>
;
; {"Required" {:type "object"
; :description "I'm required"
; :example {:name "Iines"
; :title "Ankka"}
; :minProperties 1
; :required [:address]
; :properties {:name {:type "string"}
; :title {:type "string"}
; :address {:$ref "#/definitions/RequiredAddress"}}
; :additionalProperties false}
; "RequiredAddress" {:type "object"
; :description "Streename"
; :example "Ankkalinna 1"
; :properties {:street {:type "string"
; :description "description here"}}
; :required [:street]
; :additionalProperties false}}
To release a version, set the project.clj version to the one you want to release, but with a -SNAPSHOT
suffix.
Then create a commit reading "Release :{major,minor,patch,alpha,beta,rc}" based on whether you want the next development version to be a major/minor/patch/alpha/beta/rc increment.
Push to master, and the GitHub Actions release will release the jar to clojars, then bump the version on the master branch.
Copyright © 2014-2018 Metosin Oy
Distributed under the Eclipse Public License, the same as Clojure.