HORIZON HASKELLDocslts/ghc-9.10.x248f8f02026-10-05Search names, modules, packages, or :: a typeCtrl K

GHC 9.10.3 · lts/ghc-9.10.x · 248f8f0 · 2026-10-05

Moduleservant-openapi3-2.0.1.6Haskell2010

Servant.OpenApi

This module provides means to generate and manipulate OpenApi specification for servant APIs.

OpenApi is a project used to describe and document RESTful APIs.

The OpenApi specification defines a set of files required to describe such an API. These files can then be used by the OpenApi-UI project to display the API and OpenApi-Codegen to generate clients in various languages. Additional utilities can also take advantage of the resulting files, such as testing tools.

For more information see OpenApi documentation.

  • 1 class
  • 3 values

How to use this library

0 declarations

This section explains how to use this library to generate OpenApi specification, modify it and run automatic tests for a servant API.

For the purposes of this section we will use this servant API:

Example10 expressions
data User = User { name :: String, age :: Int } deriving (Show, Generic, Typeable)newtype UserId = UserId Integer deriving (Show, Generic, Typeable, ToJSON)instance ToJSON Userinstance ToSchema Userinstance ToSchema UserIdinstance ToParamSchema UserIdtype GetUsers = Get '[JSON] [User]type GetUser  = Capture "user_id" UserId :> Get '[JSON] Usertype PostUser = ReqBody '[JSON] User :> Post '[JSON] UserIdtype UserAPI  = GetUsers :<|> GetUser :<|> PostUser

Here we define a user API with three endpoints. GetUsers endpoint returns a list of all users. GetUser returns a user given his/her ID. PostUser creates a new user and returns his/her ID.

Generate OpenApi

In order to generate OpenApi specification for a servant API, just use toOpenApi:

Example1 expression
BSL8.putStrLn $ encodePretty $ toOpenApi (Proxy :: Proxy UserAPI){    "components": {        "schemas": {            "User": {                "properties": {                    "age": {                        "maximum": 9223372036854775807,                        "minimum": -9223372036854775808,                        "type": "integer"                    },                    "name": {                        "type": "string"                    }                },                "required": [                    "name",                    "age"                ],                "type": "object"            },            "UserId": {                "type": "integer"            }        }    },    "info": {        "title": "",        "version": ""    },    "openapi": "3.0.0",    "paths": {        "/": {            "get": {                "responses": {                    "200": {                        "content": {                            "application/json;charset=utf-8": {                                "schema": {                                    "items": {                                        "$ref": "#/components/schemas/User"                                    },                                    "type": "array"                                }                            }                        },                        "description": ""                    }                }            },            "post": {                "requestBody": {                    "content": {                        "application/json;charset=utf-8": {                            "schema": {                                "$ref": "#/components/schemas/User"                            }                        }                    }                },                "responses": {                    "200": {                        "content": {                            "application/json;charset=utf-8": {                                "schema": {                                    "$ref": "#/components/schemas/UserId"                                }                            }                        },                        "description": ""                    },                    "400": {                        "description": "Invalid `body`"                    }                }            }        },        "/{user_id}": {            "get": {                "parameters": [                    {                        "in": "path",                        "name": "user_id",                        "required": true,                        "schema": {                            "type": "integer"                        }                    }                ],                "responses": {                    "200": {                        "content": {                            "application/json;charset=utf-8": {                                "schema": {                                    "$ref": "#/components/schemas/User"                                }                            }                        },                        "description": ""                    },                    "404": {                        "description": "`user_id` not found"                    }                }            }        }    }}

By default toOpenApi will generate specification for all API routes, parameters, headers, responses and data schemas.

For some parameters it will also add 400 and/or 404 responses with a description mentioning parameter name.

Data schemas come from ToParamSchema and ToSchema classes.

Annotate

While initially generated OpenApi looks good, it lacks some information it can't get from a servant API.

We can add this information using field lenses from Data.OpenApi:

Example1 expression
:{BSL8.putStrLn $ encodePretty $ toOpenApi (Proxy :: Proxy UserAPI)  & info.title        .~ "User API"  & info.version      .~ "1.0"  & info.description  ?~ "This is an API for the Users service"  & info.license      ?~ "MIT"  & servers           .~ ["https://example.com"]:}{    "components": {        "schemas": {            "User": {                "properties": {                    "age": {                        "maximum": 9223372036854775807,                        "minimum": -9223372036854775808,                        "type": "integer"                    },                    "name": {                        "type": "string"                    }                },                "required": [                    "name",                    "age"                ],                "type": "object"            },            "UserId": {                "type": "integer"            }        }    },    "info": {        "description": "This is an API for the Users service",        "license": {            "name": "MIT"        },        "title": "User API",        "version": "1.0"    },    "openapi": "3.0.0",    "paths": {        "/": {            "get": {                "responses": {                    "200": {                        "content": {                            "application/json;charset=utf-8": {                                "schema": {                                    "items": {                                        "$ref": "#/components/schemas/User"                                    },                                    "type": "array"                                }                            }                        },                        "description": ""                    }                }            },            "post": {                "requestBody": {                    "content": {                        "application/json;charset=utf-8": {                            "schema": {                                "$ref": "#/components/schemas/User"                            }                        }                    }                },                "responses": {                    "200": {                        "content": {                            "application/json;charset=utf-8": {                                "schema": {                                    "$ref": "#/components/schemas/UserId"                                }                            }                        },                        "description": ""                    },                    "400": {                        "description": "Invalid `body`"                    }                }            }        },        "/{user_id}": {            "get": {                "parameters": [                    {                        "in": "path",                        "name": "user_id",                        "required": true,                        "schema": {                            "type": "integer"                        }                    }                ],                "responses": {                    "200": {                        "content": {                            "application/json;charset=utf-8": {                                "schema": {                                    "$ref": "#/components/schemas/User"                                }                            }                        },                        "description": ""                    },                    "404": {                        "description": "`user_id` not found"                    }                }            }        }    },    "servers": [        {            "url": "https://example.com"        }    ]}

It is also useful to annotate or modify certain endpoints. subOperations provides a convenient way to zoom into a part of an API.

subOperations sub api traverses all operations of the api which are also present in sub. Furthermore, sub is required to be an exact sub API of @api. Otherwise it will not typecheck.

Data.OpenApi.Operation provides some useful helpers that can be used with subOperations. One example is applying tags to certain endpoints:

Example3 expressions
let getOps  = subOperations (Proxy :: Proxy (GetUsers :<|> GetUser)) (Proxy :: Proxy UserAPI)let postOps = subOperations (Proxy :: Proxy PostUser) (Proxy :: Proxy UserAPI):{BSL8.putStrLn $ encodePretty $ toOpenApi (Proxy :: Proxy UserAPI)  & applyTagsFor getOps  ["get"  & description ?~ "GET operations"]  & applyTagsFor postOps ["post" & description ?~ "POST operations"]:}{    "components": {        "schemas": {            "User": {                "properties": {                    "age": {                        "maximum": 9223372036854775807,                        "minimum": -9223372036854775808,                        "type": "integer"                    },                    "name": {                        "type": "string"                    }                },                "required": [                    "name",                    "age"                ],                "type": "object"            },            "UserId": {                "type": "integer"            }        }    },    "info": {        "title": "",        "version": ""    },    "openapi": "3.0.0",    "paths": {        "/": {            "get": {                "responses": {                    "200": {                        "content": {                            "application/json;charset=utf-8": {                                "schema": {                                    "items": {                                        "$ref": "#/components/schemas/User"                                    },                                    "type": "array"                                }                            }                        },                        "description": ""                    }                },                "tags": [                    "get"                ]            },            "post": {                "requestBody": {                    "content": {                        "application/json;charset=utf-8": {                            "schema": {                                "$ref": "#/components/schemas/User"                            }                        }                    }                },                "responses": {                    "200": {                        "content": {                            "application/json;charset=utf-8": {                                "schema": {                                    "$ref": "#/components/schemas/UserId"                                }                            }                        },                        "description": ""                    },                    "400": {                        "description": "Invalid `body`"                    }                },                "tags": [                    "post"                ]            }        },        "/{user_id}": {            "get": {                "parameters": [                    {                        "in": "path",                        "name": "user_id",                        "required": true,                        "schema": {                            "type": "integer"                        }                    }                ],                "responses": {                    "200": {                        "content": {                            "application/json;charset=utf-8": {                                "schema": {                                    "$ref": "#/components/schemas/User"                                }                            }                        },                        "description": ""                    },                    "404": {                        "description": "`user_id` not found"                    }                },                "tags": [                    "get"                ]            }        }    },    "tags": [        {            "description": "GET operations",            "name": "get"        },        {            "description": "POST operations",            "name": "post"        }    ]}

This applies "get" tag to the GET endpoints and "post" tag to the POST endpoint of the User API.

Test

Automatic generation of data schemas uses ToSchema instances for the types used in a servant API. But to encode/decode actual data servant uses different classes. For instance in UserAPI User is always encoded/decoded using ToJSON and FromJSON instances.

To be sure your Haskell server/client handles data properly you need to check that ToJSON instance always generates values that satisfy schema produced by ToSchema instance.

With validateEveryToJSON it is possible to test all those instances automatically, without having to write down every type:

Example3 expressions
instance Arbitrary User where arbitrary = User <$> arbitrary <*> arbitraryinstance Arbitrary UserId where arbitrary = UserId <$> arbitraryhspec $ validateEveryToJSON (Proxy :: Proxy UserAPI)[User]......User......UserId......Finished in ... seconds...3 examples, 0 failures...

Although servant is great, chances are that your API clients don't use Haskell. In many cases swagger.json serves as a specification, not a Haskell type.

In this cases it is a good idea to store generated and annotated OpenApi in a swagger.json file under a version control system (such as Git, Subversion, Mercurial, etc.).

It is also recommended to version API based on changes to the swagger.json rather than changes to the Haskell API.

See TodoSpec.hs for an example of a complete test suite for a swagger specification.

Serve

If you're implementing a server for an API, you might also want to serve its OpenApi specification.

See Todo.hs for an example of a server.

HasOpenApi class

1 declaration
classclass HasOpenApi (api :: k) where
#

Generate a OpenApi specification for a servant API.

To generate OpenApi specification, your data types need ToParamSchema and/or ToSchema instances.

ToParamSchema is used for Capture, QueryParam and Header. ToSchema is used for ReqBody and response data types.

You can easily derive those instances via Generic. For more information, refer to openapi3 documentation.

Example:

newtype Username = Username String deriving (Generic, ToText)

instance ToParamSchema Username

data User = User
  { username :: Username
  , fullname :: String
  } deriving (Generic)

instance ToJSON User
instance ToSchema User

type MyAPI = QueryParam "username" Username :> Get '[JSON] User

myOpenApi :: OpenApi
myOpenApi = toOpenApi (Proxy :: Proxy MyAPI)

Methods

Instances29HasOpenApi, …

Manipulation

1 declaration

Testing

2 declarations
valuevalidateEveryToJSON
  1. :: TMap (Every '[Typeable, Show, Arbitrary, ToJSON, ToSchema]) (BodyTypes JSON api)
  2. => proxy api

    Servant API.

  3. -> Spec
#

Verify that every type used with JSON content type in a servant API has compatible ToJSON and ToSchema instances using validateToJSON.

NOTE: validateEveryToJSON does not perform string pattern validation. See validateEveryToJSONWithPatternChecker.

validateEveryToJSON will produce one prop specification for every type in the API. Each type only gets one test, even if it occurs multiple times in the API.

Example7 expressions
data User = User { name :: String, age :: Maybe Int } deriving (Show, Generic, Typeable)newtype UserId = UserId String deriving (Show, Generic, Typeable, ToJSON, Arbitrary)instance ToJSON Userinstance ToSchema Userinstance ToSchema UserIdinstance Arbitrary User where arbitrary = User <$> arbitrary <*> arbitrarytype UserAPI = (Capture "user_id" UserId :> Get '[JSON] User) :<|> (ReqBody '[JSON] User :> Post '[JSON] UserId)
Example1 expression
hspec $ context "ToJSON matches ToSchema" $ validateEveryToJSON (Proxy :: Proxy UserAPI)ToJSON matches ToSchema  User......  UserId......Finished in ... seconds...2 examples, 0 failures...

For the test to compile all body types should have the following instances:

If any of the instances is missing, you'll get a descriptive type error:

Example5 expressions
data Contact = Contact { fullname :: String, phone :: Integer } deriving (Show, Generic)instance ToJSON Contactinstance ToSchema Contacttype ContactAPI = Get '[JSON] Contacthspec $ validateEveryToJSON (Proxy :: Proxy ContactAPI)......No instance for (Arbitrary Contact)...  arising from a use of ‘validateEveryToJSON’...