Announcing servant-swagger and swagger2


Servant is not the first project to provide a unified way of documenting APIs. There is API Blueprint, RAML, Apiary, and finally swagger. While these Web API description languages are not also web frameworks , they are generally very mature, and have some amazing tooling. For example, take a look at what swagger-ui, a client-side HTML, CSS, and JS bundle, does with your swagger API description here.

As you can see, it’s a very convenient and approachable way of exploring your API. In addition to an easily-navigable structure, you can build up requests and send them to your server, and see its responses.

But it doesn’t end there. If you have a swagger specification of your API, you can also take advantage of the large variety of languages for which you can generate a client library automatically. You don’t even need to build the Java code - you can just use the “Generate Client” button in the beautiful swagger editor.

There are a wide array of other tools that support swagger. Obviously, having access to them would be a great boon. The problem so far has been that writing and maintaining a swagger specification, that you can be sure matches your service, is hard work.

swagger2 and servant-swagger

Thankfully David Johnson and Nickolay Kudasov have written two Haskell libraries, swagger2 and servant-swagger, that automate nearly all of that process for servant APIs. They use the mechanism that guides most of the servant ecosystem — interpreters for the type-level DSL for APIs that is servant — to generate a swagger spec for that API.

Let’s see how it is used; as an example, we’re going to take the Gists part of the GitHub API v3. For the purpose of this post we will ignore authentication and consider only GET requests which do not require one. Furthermore, we’ll use simplified representation for the responses (i.e. we are also ignoring some fields of the response objects).

First the imports and pragmas (this is a literate haskell file):

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
module Gists where

import Control.Lens
import Data.Aeson
import Data.Aeson.Types (camelTo2)
import qualified Data.Aeson.Types as JSON
import qualified Data.ByteString.Lazy.Char8 as BL8
import Data.HashMap.Strict (HashMap)
import Data.Proxy
import Data.Swagger
import Data.Text (Text)
import Data.Time (UTCTime)
import GHC.Generics (Generic)
import Servant
import Servant.Swagger

The API:

type GitHubGistAPI
    = "users" :> Capture "username" Username :> "gists" :> QueryParam "since" UTCTime :> Get '[JSON] [Gist]
 :<|> "gists" :> GistsAPI

type GistsAPI
    = "public"  :> QueryParam "since" UTCTime :> Get '[JSON] [Gist]
 :<|> "starred" :> QueryParam "since" UTCTime :> Get '[JSON] [Gist]
 :<|> Capture "id" GistId :> GistAPI

type GistAPI
    = Get '[JSON] Gist
 :<|> Capture "sha" Revision :> Get '[JSON] Gist

api :: Proxy GitHubGistAPI
api = Proxy

Data types:

newtype Username = Username Text deriving (Generic, ToText, FromJSON)

newtype GistId = GistId Text deriving (Generic, ToText, FromJSON)

newtype SHA = SHA Text deriving (Generic, ToText)

type Revision = SHA

data Gist = Gist
  { gistId          :: GistId
  , gistDescription :: Text
  , gistOwner       :: Owner
  , gistFiles       :: HashMap FilePath GistFile
  , gistTruncated   :: Bool
  , gistComments    :: Integer
  , gistCreatedAt   :: UTCTime
  , gistUpdatedAt   :: UTCTime
  } deriving (Generic)

data OwnerType = User | Organization
  deriving (Generic)

data Owner = Owner
  { ownerLogin      :: Username
  , ownerType       :: OwnerType
  , ownerSiteAdmin  :: Bool
  } deriving (Generic)

data GistFile = GistFile
  { gistfileSize      :: Integer
  , gistfileLanguage  :: Text
  , gistfileRawUrl    :: Text
  } deriving (Generic)

FromJSON instances:

modifier :: String -> String
modifier = drop 1 . dropWhile (/= '_') . camelTo2 '_'

prefixOptions :: JSON.Options
prefixOptions = JSON.defaultOptions { JSON.fieldLabelModifier = modifier }

instance FromJSON OwnerType
instance FromJSON Owner    where parseJSON = genericParseJSON prefixOptions
instance FromJSON GistFile where parseJSON = genericParseJSON prefixOptions
instance FromJSON Gist     where parseJSON = genericParseJSON prefixOptions

So far this is what you would usually have when working with servant. Now to generate Swagger specification we need to define schemas for our types. This is done with ToParamSchema and ToSchema instances:

prefixSchemaOptions :: SchemaOptions
prefixSchemaOptions = defaultSchemaOptions { fieldLabelModifier = modifier }

instance ToParamSchema SHA
instance ToParamSchema Username
instance ToParamSchema GistId

instance ToSchema Username
instance ToSchema GistId
instance ToSchema OwnerType
instance ToSchema Owner    where declareNamedSchema = genericDeclareNamedSchema prefixSchemaOptions
instance ToSchema GistFile where declareNamedSchema = genericDeclareNamedSchema prefixSchemaOptions
instance ToSchema Gist     where declareNamedSchema = genericDeclareNamedSchema prefixSchemaOptions

These will give us a generically-derived Swagger schema (which is sort of a deterministic version of JSON Schema).

Part of the swagger2 package, Schema and ParamSchema can be quite useful in their own right if you want to e.g. respond with a schema in case of bad request bodies, or OPTIONS requests.

The next step will traverse the GitHubGistAPI, gathering information about it and swagger2 schemas to generate a Swagger value:

swaggerDoc1 :: Swagger
swaggerDoc1 = toSwagger api

Now we can generate the swagger documentation:

genSwaggerDoc1 :: IO ()
genSwaggerDoc1 = BL8.putStr $ encode swaggerDoc1

You can attach more information to your Swagger doc quite easily, using the lenses provided by swagger2:

swaggerDoc2 :: Swagger
swaggerDoc2 = swaggerDoc1
  & host ?~ ""
  & info.title .~ "GitHub Gists API"
  & info.version .~ "v3"
main :: IO ()
main = BL8.putStr $ encode swaggerDoc2

Which results in this.

There’s a lot more you can do with both servant-swagger and swagger2 — write manual ToSchema instances for more detailed information, conveniently add tags or change responses of parts of your API, use convenient lenses to modify any part of your schema, generate automatic tests, etc.

Check out the servant-swagger and swagger2 docs for more.

These two new packages vastly expand the landscape of tools within easy reach of application developers using servant. Time to explore that landscape!

On a related note, Masahiro Yamauchi has recently added Servant codegen for Swagger. So not only can you generate a swagger description for your servant server, but you can also generate the servant description from a swagger one too!

Posted on February 6, 2016 by David Johnson, Nickolay Kudasov, Julian Arni