File: README.md
Connexion is a modern Python web framework that makes spec-first and api-first development easy.
You describe your API in an OpenAPI (or Swagger) specification with as much
detail as you want and Connexion will guarantee that it works as you specified.
It works either standalone, or in combination with any ASGI or WSGI-compatible framework!
โจ Features
Connexion provides the following functionality based on your specification:
- ๐ Automatic route registration, no
@routedecorators needed - ๐ Authentication, split from your application logic
- ๐ Request and response validation of headers, parameters, and body
- ๐ฌ Parameter parsing and injection, no request object needed
- ๐จ Response serialization, you can return regular Python objects
- ๐บ A Swagger UI console with live documentation and โtry it outโ feature
- ๐งฉ Pluggability, in all dimensions
Connexion also helps you write your OpenAPI specification and develop against it by providing a command line interface which lets you test and mock your specification.
connexion run openapi.yaml๐ซถ Sponsors
Sponsors help us dedicate time to maintain Connexion. Want to help?
๐ชค Why Connexion
With Connexion, you write the spec first. Connexion then calls your Python
code, handling the mapping from the specification to the code. This
incentivizes you to write the specification so that all of your
developers can understand what your API does, even before you write a
single line of code.
If multiple teams depend on your APIs, you can use Connexion to easily
send them the documentation of your API. This guarantees that your API will
follow the specification that you wrote. This is a different process from
the one offered by most frameworks, which generate a specification
after you've written the code.
Some disadvantages of generating specifications based on code is that
they often end up lacking details or mix your documentation with the implementation
logic of your application.
โ๏ธ How to Use
Installation
You can install connexion using pip:
$ pip install connexionConnexion provides 'extras' with optional dependencies to unlock additional features:
swagger-ui: Enables a Swagger UI console for your application.uvicorn: Enables to run the your application usingapp.run()for
development instead of using an external ASGI server.flask: Enables theFlaskAppto build applications compatible with the Flask
ecosystem.
You can install them as follows:
$ pip install connexion[swagger-ui]
$ pip install connexion[swagger-ui,uvicorn]Creating your application
Connexion can be used either as a standalone application or as a middleware wrapping an existing
ASGI (or WSGI) application written using a different framework. The standalone application can be
built using either the AsyncApp or FlaskApp.
The
AsyncAppis a lightweight application with native asynchronous support. Use it if you
are starting a new project and have no specific reason to use one of the other options.pythonfrom connexion import AsyncApp app = AsyncApp(__name__)The
FlaskAppleverages theFlaskframework, which is useful if you're migrating from
connexion 2.X or you want to leverage theFlaskecosystem.pythonfrom connexion import FlaskApp app = FlaskApp(__name__)The
ConnexionMiddlewarecan be wrapped around any existing ASGI or WSGI application.
Use it if you already have an application written in a different framework and want to add
functionality provided by connexionpythonfrom asgi_framework import App from connexion import ConnexionMiddleware app = App(__name__) app = ConnexionMiddleware(app)
Registering an API
While you can register individual routes on your application, Connexion really shines when you
register an API defined by an OpenAPI (or Swagger) specification.
The operation described in your specification is automatically linked to your Python view function via the operationId
run.py
def post_greeting(name: str, greeting: str): # Paramaeters are automatically unpacked
return f"{greeting} {name}", 200 # Responses are automatically serialized
app.add_api("openapi.yaml")openapi.yaml
...
paths:
/greeting/{name}:
post:
operationId: run.post_greeting
responses:
'200':
content:
text/plain:
schema:
type: string
parameters:
- name: name
in: path
required: true
schema:
type: string
- name: greeting
in: query
required: true
schema:
type: stringRunning your application
If you installed connexion using connexion[uvicorn], you can run it using therun method. This is only recommended for development:
app.run()In production, run your application using an ASGI server such as uvicorn. If you defined yourapp in a python module called run.py, you can run it as follows:
$ uvicorn run:appOr with gunicorn:
$ gunicorn -k uvicorn.workers.UvicornWorker run:appNow you're able to run and use Connexion!
See the examples folder for more examples.
๐ Changes
A full changelog is maintained on the GitHub releases page.
๐คฒ Contributing
We welcome your ideas, issues, and pull requests. Just follow the
usual/standard GitHub practices.
For easy development, install connexion using poetry with all extras, and
install the pre-commit hooks to automatically run black formatting and static analysis checks.
pip install poetry
poetry install --all-extras
pre-commit installYou can find out more about how Connexion works and where to apply your changes by having a look
at our architecture.
Unless you explicitly state otherwise in advance, any non trivial
contribution intentionally submitted for inclusion in this project by you
to the steward of this repository shall be under the
terms and conditions of Apache License 2.0 written below, without any
additional copyright information, terms or conditions.
๐ Thanks
We'd like to thank all of Connexion's contributors for working on this
project, Swagger/OpenAPI for their support, and Zalando for originally developing and releasing Connexion.
๐ Recommended Resources
About the advantages of working spec-first:
Tools to help you work spec-first:
File: docs/cli.rst
Command-Line Interface
For convenience Connexion provides a command-line interface
(CLI). This interface aims to be a starting point in developing or
testing OpenAPI specifications with Connexion.
Running an OpenAPI specification
The subcommand run of Connexion's CLI makes it easy to run OpenAPI
specifications directly even before any operation handler function gets
implemented. This allows you to verify and inspect how your API will
work with Connexion.
To run your specification, execute in your shell:
.. code-block:: bash
$ connexion run your_api.yaml --stubThis command will tell Connexion to run the your_api.yaml
specification file attaching a stub operation (--stub) to the
unavailable operations/functions of your API, which will return a 501 Not Implemented response.
The basic usage of this command is:
.. code-block:: bash
$ connexion run [OPTIONS] SPEC_FILE [BASE_MODULE_PATH]Where:
- SPEC_FILE: Your OpenAPI specification file in YAML format. Can also be given
as a URL, which will be automatically downloaded. - BASE_MODULE_PATH (optional): filesystem path where the API endpoints
handlers are going to be imported from. In short, where your Python
code is saved.
There are more options available for the run command, for a full
list run:
.. code-block:: bash
$ connexion run --helpRunning a mock server
You can run a simple server which returns example responses on every request.
The example responses can be defined in the examples response property of
the OpenAPI specification. If no examples are specified, and you have installed connexion with the mock extra (pip install connexion[mock]), an example is generated based on the provided schema.
Your API specification file is not required to have any operationId.
.. code-block:: bash
$ connexion run your_api.yaml --mock=all
$ connexion run https://raw.githubusercontent.com/spec-first/connexion/main/examples/helloworld_async/spec/openapi.yaml --mock=allFile: docs/context.rst
Context
The ContextMiddleware included in Connexion provides some information about the current request
context as thread-safe request-level global variables.
You can access them by importing them from connexion.context:
.. code-block:: python
from connexion.context import context, operation, receive, request, scope
from connexion import request # alias for connexion.context.requestNote that when trying to access these context variables outside of the request handling flow, or
without running the ContextMiddleware, the following RuntimeError will be thrown:
.. code-block:: text
RuntimeError: Working outside of operation context. Make sure your app is wrapped in a
ContextMiddleware and you're processing a request while accessing the context.See below for an explanation of the different variables.
request
A Request object representing the incoming request. This is an instance of theConnexionRequest.
.. dropdown:: View a detailed reference of the ConnexionRequest class
:icon: eye
.. autoclass:: connexion.lifecycle.ConnexionRequest
:noindex:
:members:
:undoc-members:
:inherited-members:Some of the methods and attributes are coroutines that can only be accessed from an async
context. When using the FlaskApp, you might want to import the Flask request instead:
.. code-block:: python
from flask import requestoperation
An Operation object representing the matched operation from your OpenAPI specification.
.. tab-set::
.. tab-item:: OpenAPI 3
:sync: OpenAPI 3
When using OpenAPI 3, this is an instance of the ``OpenAPIOperation`` class.
.. dropdown:: View a detailed reference of the ``OpenAPIOperation`` class
:icon: eye
.. autoclass:: connexion.operations.OpenAPIOperation
:members:
:undoc-members:
:inherited-members:
.. tab-item:: Swagger 2
:sync: Swagger 2
When using Swagger 2, this is an instance of the ``Swagger2Operation`` class.
.. dropdown:: View a detailed reference of the ``Swagger2Operation`` class
:icon: eye
.. autoclass:: connexion.operations.Swagger2Operation
:members:
:undoc-members:
:inherited-members:scope
The ASGI scope as received by the ContextMiddleware, thus containing any changes propagated by
upstream middleware. The ASGI scope is presented as a dict. Please refer to the ASGI spec_
for more information on its contents.
context.context
A dict containing the information from the security middleware:
.. code-block:: python
{
"user": ... # User information from authentication
"token_info": ... # Token information from authentication
}Third party or custom middleware might add additional fields to this.
receive
.. warning:: Advanced usage
The receive channel as received by the ContextMiddleware. Note that the receive channel might
already be read by other parts of Connexion (eg. when accessing the body via the Request, or
when it is injected into your Python function), and that reading it yourself might make it
unavailable for those parts of the application.
The receive channel can only be accessed from an async context and is therefore not relevant
when using the FlaskApp.
.. _ASGI spec: https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope
File: docs/cookbook.rst
Connexion Cookbook
This page provides recipes with Connexion as an ingredient.
CORS
You can enable CORS (Cross-origin resource sharing) by leveraging the CORSMiddleware_ offered by
Starlette. You can add it to your application, ideally in front of the RoutingMiddleware.
.. tab-set::
.. tab-item:: AsyncApp
:sync: AsyncApp
.. code-block:: python
from pathlib import Path
from connexion import AsyncApp
from connexion.middleware import MiddlewarePosition
from starlette.middleware.cors import CORSMiddleware
app = AsyncApp(__name__)
app.add_middleware(
CORSMiddleware,
position=MiddlewarePosition.BEFORE_EXCEPTION,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_api("openapi.yaml")
if __name__ == "__main__":
app.run(f"{Path(__file__).stem}:app", port=8080)
.. dropdown:: View a detailed reference of the ``add_middleware`` method
:icon: eye
.. automethod:: connexion.AsyncApp.add_middleware
:noindex:
.. tab-item:: FlaskApp
:sync: FlaskApp
.. code-block:: python
from pathlib import Path
from connexion import FlaskApp
from connexion.middleware import MiddlewarePosition
from starlette.middleware.cors import CORSMiddleware
app = FlaskApp(__name__)
app.add_middleware(
CORSMiddleware,
position=MiddlewarePosition.BEFORE_EXCEPTION,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_api("openapi.yaml")
if __name__ == "__main__":
app.run(f"{Path(__file__).stem}:app", port=8080)
.. dropdown:: View a detailed reference of the ``add_middleware`` method
:icon: eye
.. automethod:: connexion.FlaskApp.add_middleware
:noindex:
.. tab-item:: ConnexionMiddleware
:sync: ConnexionMiddleware
.. code-block:: python
from pathlib import Path
from asgi_framework import App
from connexion import ConnexionMiddleware
from starlette.middleware.cors import CORSMiddleware
app = App(__name__)
app = ConnexionMiddleware(app)
app.add_middleware(
CORSMiddleware,
position=MiddlewarePosition.BEFORE_EXCEPTION,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_api("openapi.yaml")
if __name__ == "__main__":
app.run(f"{Path(__file__).stem}:app", port=8080)
.. dropdown:: View a detailed reference of the ``add_middleware`` method
:icon: eye
.. automethod:: connexion.ConnexionMiddleware.add_middleware
:noindex:.. _CORSMiddleware: https://www.starlette.io/middleware/#corsmiddleware
Reverse Proxy
When running behind a reverse proxy with stripped path prefix, you need to configure your
application to properly handle this.
Single known path prefix
''''''''''''''''''''''''
If there is only a single known prefix your application will be running behind, you can simply
pass this path prefix as the root_path to your ASGI server:
.. code-block:: bash
$ uvicorn run:app --root-path <root_path>.. code-block:: bash
$ gunicorn -k uvicorn.workers.UvicornWorker run:app --root-path <root_path>Dynamic path prefix
'''''''''''''''''''
If you are running behind multiple proxies, or the path is not known, you can wrap your
application in a ReverseProxied middleware as shown in this example_.
.. _this example: https://github.com/spec-first/connexion/tree/main/examples/reverseproxy
File: docs/exceptions.rst
Exception Handling
Connexion allows you to register custom error handlers to convert Python Exceptions into HTTP
problem responses.
You can register error handlers on:
- The exception class to handle
If this exception class is raised somewhere in your application or the middleware stack,
it will be passed to your handler. - The HTTP status code to handle
Connexion will raisestarlette.HTTPExceptionerrors when it encounters any issues
with a request or response. You can intercept these exceptions with specific status codes
if you want to return custom responses.
.. tab-set::
.. tab-item:: AsyncApp
:sync: AsyncApp
.. code-block:: python
from connexion import AsyncApp
from connexion.lifecycle import ConnexionRequest, ConnexionResponse
def not_found(request: ConnexionRequest, exc: Exception) -> ConnexionResponse:
return ConnexionResponse(status_code=404, body=json.dumps({"error": "NotFound"}))
app = AsyncApp(__name__)
app.add_error_handler(FileNotFoundError, not_found)
app.add_error_handler(404, not_found)
.. dropdown:: View a detailed reference of the ``add_error_handler`` method
:icon: eye
.. automethod:: connexion.AsyncApp.add_error_handler
:noindex:
.. tab-item:: FlaskApp
:sync: FlaskApp
.. code-block:: python
from connexion import FlaskApp
from connexion.lifecycle import ConnexionRequest, ConnexionResponse
def not_found(request: ConnexionRequest, exc: Exception) -> ConnexionResponse:
return ConnexionResponse(status_code=404, body=json.dumps({"error": "NotFound"}))
app = FlaskApp(__name__)
app.add_error_handler(FileNotFoundError, not_found)
app.add_error_handler(404, not_found)
.. dropdown:: View a detailed reference of the ``add_error_handler`` method
:icon: eye
.. automethod:: connexion.FlaskApp.add_error_handler
:noindex:
.. note::
.. warning::
โ ๏ธ **The following is not recommended as it complicates the exception handling logic,**
You can also register error handlers on the underlying flask application directly.
.. code-block:: python
flask_app = app.app
flask_app.register_error_handler(FileNotFoundError, not_found)
flask_app.register_error_handler(404, not_found)
`Flask documentation`_
Error handlers registered this way:
- Will only intercept exceptions thrown in the application, not in the Connexion
middleware.
- Can intercept exceptions before they reach the error handlers registered on the
connexion app.
- When registered on status code, will intercept only
``werkzeug.exceptions.HTTPException`` thrown by werkzeug / Flask not
``starlette.exceptions.HTTPException``.
.. tab-item:: ConnexionMiddleware
:sync: ConnexionMiddleware
.. code-block:: python
from asgi_framework import App
from connexion import ConnexionMiddleware
from connexion.lifecycle import ConnexionRequest, ConnexionResponse
def not_found(request: ConnexionRequest, exc: Exception) -> ConnexionResponse:
return ConnexionResponse(status_code=404, body=json.dumps({"error": "NotFound"}))
app = App(__name__)
app = ConnexionMiddleware(app)
app.add_error_handler(FileNotFoundError, not_found)
app.add_error_handler(404, not_found)
.. dropdown:: View a detailed reference of the ``add_error_handler`` method
:icon: eye
.. automethod:: connexion.ConnexionMiddleware.add_error_handler
:noindex:
.. note::
This might not catch ``HTTPExceptions`` with the same status code raised by
your wrapped ASGI/WSGI framework... note::
Error handlers can be ``async`` coroutines as well... _Flask documentation: https://flask.palletsprojects.com/en/latest/errorhandling/#error-handlers
Default Exception Handling
By default connexion exceptions are JSON serialized according toProblem Details for HTTP APIs_
Application can return errors using connexion.problem.problem or raise exceptions that inherit
either from connexion.ProblemException or one of its subclasses to achieve the same behavior.
Using this, we can rewrite the handler above:
.. code-block:: python
from connexion.lifecycle import ConnexionRequest, ConnexionResponse
from connexion.problem import problem
def not_found(request: ConnexionRequest, exc: Exception) -> ConnexionResponse:
return problem(
title="NotFound",
detail="The requested resource was not found on the server",
status=404,
).. dropdown:: View a detailed reference of the :code:problem function
:icon: eye
.. autofunction:: connexion.problem.problem.. _Problem Details for HTTP APIs: https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
Connexion Exceptions
There are several exception types in connexion that contain extra information to help you render appropriate
messages to your user beyond the default description and status code:
.. automodule:: connexion.exceptions
:members:
:show-inheritance:
:member-order: bysource
File: docs/index.rst
.. Connexion documentation master file, created by
sphinx-quickstart on Wed Jun 17 12:09:55 2015.
You can adapt this file completely to your liking, but it should at least
contain the root toctree directive.
.. important::
๐ข **Connexion 3 was recently released!**
If you are upgrading from Connexion 2, refer to the :doc:`v3` section.Welcome to Connexion's documentation!
Connexion is a modern Python web framework that makes spec-first and api-first development easy. You
describe your API in an OpenAPI_ (or swagger) specification with as much detail as you want and
Connexion will guarantee that it works as you specified.
Connexion Features
Based on your specification, Connexion provides the following functionality:
- Automatic routing to your Python functions
- Authentication
- Request validation
- Parameter parsing and injection
- Response serialization
- Response validation
- A Swagger UI console with live documentation and 'try it out' feature
Connexion also helps you write your OpenAPI specification and develop against it by providing a
command line interface which lets you test and mock your specification.
.. code-block:: bash
connexion run openapi.yamlSponsors
.. image:: https://raw.githubusercontent.com/spec-first/connexion/main/docs/images/sponsors/ML6.png
:alt: ML6
:height: 100px
:target: https://www.ml6.eu
.. image:: https://raw.githubusercontent.com/spec-first/connexion/main/docs/images/sponsors/Fern.png
:alt: Fern
:height: 100px
:target: https://www.devmark.ai/fern/?utm_source=connexion&utm_loc=website&utm_type=logo
Sponsors help us dedicate time to maintain Connexion. Want to help?
.. raw:: html
|
Why Connexion?
Being spec-first is what makes Connexion unique in the Python ecosystem. With Connexion, you write
your API specification first, and automatically get a lot of functionality. With all other popular
Python web frameworks, you write your functionality first, and automatically get your specification.
We choose the spec-first approach because it:
- Stimulates thinking about the design of your API and enables quick feedback loops
- Creates a common understanding of how the API should work
- Allows server and client development in parallel
- Enables contract testing
- Allows for orchestrating multiple layers of your API stack from one contract (eg. API Gateway)
For a more detailed explanation about the benefits of working spec-first, or an overview of helpful
tooling, have a look at our recommended resources_.
Documentation
.. toctree::
:maxdepth: 2
quickstart
routing
swagger_ui
request
response
validation
security
context
lifespan
exceptions
middleware
testing
cli
cookbook
v3
Recommended resources
About the advantages of working spec-first:
Blog Atlassian_API guidelines Zalando_Blog ML6_Blog Zalando_
Tools to help you work spec-first:
Online swagger editor_VS Code plugin_Pycharm plugin_
.. _stable version: https://connexion.readthedocs.io/en/stable/
.. _v3: https://connexion.readthedocs.io/en/latest/v3.html
.. _OpenAPI: https://openapis.org/
.. _Blog atlassian: https://www.atlassian.com/blog/technology/spec-first-api-development
.. _Blog ML6: https://blog.ml6.eu/why-we-decided-to-help-maintain-connexion-c9f449877083
.. _Blog Zalando: https://engineering.zalando.com/posts/2016/12/crafting-effective-microservices-in-python.html
.. _API guidelines Zalando: https://opensource.zalando.com/restful-api-guidelines/#api-first
.. _Online swagger editor: https://editor.swagger.io/
.. _VS Code plugin: https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi
.. _Pycharm plugin: https://plugins.jetbrains.com/plugin/14837-openapi-swagger-editor
File: docs/lifespan.rst
Lifespan
You can register lifespan handlers to run code before the app starts, or after it shuts down.
This ideal for setting up and tearing down database connections or machine learning models for
instance.
.. tab-set::
.. tab-item:: AsyncApp
:sync: AsyncApp
.. code-block:: python
import contextlib
import typing
from connexion import AsyncApp, ConnexionMiddleware, request
@contextlib.asynccontextmanager
async def lifespan_handler(app: ConnexionMiddleware) -> typing.AsyncIterator:
"""Called at startup and shutdown, can yield state which will be available on the
request."""
client = Client()
yield {"client": client}
client.close()
def route():
"""Endpoint function called when receiving a request, you can access the state
on the request here."""
client = request.state.client
client.call()
app = AsyncApp(__name__, lifespan=lifespan_handler)
.. tab-item:: FlaskApp
:sync: FlaskApp
.. code-block:: python
import contextlib
import typing
from connexion import FlaskApp, ConnexionMiddleware, request
@contextlib.asynccontextmanager
async def lifespan_handler(app: ConnexionMiddleware) -> typing.AsyncIterator:
"""Called at startup and shutdown, can yield state which will be available on the
request."""
client = Client()
yield {"client": client}
client.close()
def route():
"""Endpoint function called when receiving a request, you can access the state
on the request here."""
client = request.state.client
client.call()
app = FlaskApp(__name__, lifespan=lifespan_handler)
.. tab-item:: ConnexionMiddleware
:sync: ConnexionMiddleware
.. code-block:: python
import contextlib
import typing
from asgi_framework import App
from connexion import ConnexionMiddleware, request
@contextlib.asynccontextmanager
async def lifespan_handler(app: ConnexionMiddleware) -> typing.AsyncIterator:
"""Called at startup and shutdown, can yield state which will be available on the
request."""
client = Client()
yield {"client": client}
client.close()
def endpoint():
"""Endpoint function called when receiving a request, you can access the state
on the request here."""
client = request.state.client
client.call()
app = App(__name__)
app = ConnexionMiddleware(app, lifespan=lifespan_handler)Running lifespan in tests
If you want lifespan handlers to be called during tests, you can use the test_client as a
context manager.
.. code-block:: python
def test_homepage():
app = ... # Set up app
with app.test_client() as client:
# Application's lifespan is called on entering the block.
response = client.get("/")
assert response.status_code == 200
# And the lifespan's teardown is run when exiting the block.For more information, please refer to the Starlette documentation_.
.. _Starlette documentation: https://www.starlette.io/lifespan/
File: docs/middleware.rst
Middleware
Connexion is built as an ASGI middleware stack wrapping an application. It includes several
middlewares by default that add functionality based on the OpenAPI specification, in the
following order:
.. csv-table::
:widths: 30, 70
**ServerErrorMiddleware**, "Returns server errors for any exceptions not caught by the
ExceptionMiddleware"
**ExceptionMiddleware**, Handles exceptions raised by the middleware stack or application
**SwaggerUIMiddleware**, Adds a Swagger UI to your application
**RoutingMiddleware**, "Routes incoming requests to the right operation defined in the
specification"
**SecurityMiddleware**, "Checks incoming requests against the security defined in the
specification"
**RequestValidationMiddleware**, Validates the incoming requests against the spec
**ResponseValidationMiddleware**, "Validates the returned responses against the spec, if
activated"
**LifespanMiddleware**, "Allows registration of code to run before application start-up or
after shut-down"
**ContextMiddleware**, "Makes several request scoped context variables available to the
application"Adding middleware
You can easily add additional ASGI middleware to the middleware stack with the
:code:add_middleware method:
.. tab-set::
.. tab-item:: AsyncApp
:sync: AsyncApp
.. code-block:: python
from connexion import AsyncApp
app = AsyncApp(__name__)
app.add_middleware(MiddlewareClass, **options)
.. dropdown:: View a detailed reference of the :code:`add_middleware` method
:icon: eye
.. automethod:: connexion.AsyncApp.add_middleware
:noindex:
.. tab-item:: FlaskApp
:sync: FlaskApp
.. code-block:: python
from connexion import FlaskApp
app = FlaskApp(__name__)
app.add_middleware(MiddlewareClass, **options)
.. dropdown:: View a detailed reference of the :code:`add_middleware` method
:icon: eye
.. automethod:: connexion.FlaskApp.add_middleware
:noindex:
You can also add WSGI middleware to a ``FlaskApp``. Note that it will only be called at the
end of the middleware stack. If you need your middleware to act sooner, you will have to
use an ASGI middleware instead.
.. code-block:: python
app.add_wsgi_middleware(MiddlewareClass, **options)
.. dropdown:: View a detailed reference of the :code:`add_middleware` method
:icon: eye
.. automethod:: connexion.FlaskApp.add_wsgi_middleware
:noindex:
.. tab-item:: ConnexionMiddleware
:sync: ConnexionMiddleware
.. code-block:: python
from asgi_framework import App
from connexion import ConnexionMiddleware
app = App(__name__)
app = ConnexionMiddleware(app)
app.add_middleware(MiddlewareClass, **options)
.. dropdown:: View a detailed reference of the :code:`add_wsgi_middleware` method
:icon: eye
.. automethod:: connexion.ConnexionMiddleware.add_middleware
:noindex:Middleware order
The :code:add_middleware method takes a :code:position argument to define where in the
middleware stack it should be inserted, which should be an instance of the
:class:~connexion.middleware.MiddlewarePosition Enum. The positions below are ordered from
outer to inner, in the order they are hit by incoming requests. Note that responses hit the
middlewares in reversed order.
.. autoclass:: connexion.middleware.MiddlewarePosition
:members:
:member-order: bysource
Customizing the middleware stack
If you need more flexibility, or want to modify or delete any of the default middlewares, you can
also pass in a customized middleware stack when instantiating your application.
For example, if you would like to remove the :class:SecurityMiddleware since you are handling
Security through an API Gateway in front of your application, you can do:
.. tab-set::
.. tab-item:: AsyncApp
:sync: AsyncApp
.. code-block:: python
from connexion import AsyncApp, ConnexionMiddleware
from connexion.middleware.security import SecurityMiddleware
middlewares = [middleware for middleware in ConnexionMiddleware.default_middlewares
if middleware is not SecurityMiddleware]
app = AsyncApp(__name__, middlewares=middlewares)
.. dropdown:: View a detailed reference of the :class:`~connexion.AsyncApp`
:code:`__init__` method
:icon: eye
.. autoclass:: connexion.AsyncApp
:noindex:
.. tab-item:: FlaskApp
:sync: FlaskApp
.. code-block:: python
from connexion import FlaskApp, ConnexionMiddleware
from connexion.middleware.security import SecurityMiddleware
middlewares = [middleware for middleware in ConnexionMiddleware.default_middlewares
if middleware is not SecurityMiddleware]
app = FlaskApp(__name__, middlewares=middlewares)
.. dropdown:: View a detailed reference of the :class:`~connexion.FlaskApp`
:code:`__init__` method
:icon: eye
.. autoclass:: connexion.FlaskApp
:noindex:
.. tab-item:: ConnexionMiddleware
:sync: ConnexionMiddleware
.. code-block:: python
from asgi_framework import App
from connexion import ConnexionMiddleware
from connexion.middleware.security import SecurityMiddleware
middlewares = [middleware for middleware in ConnexionMiddleware.default_middlewares
if middleware is not SecurityMiddleware]
app = App(__name__)
app = ConnexionMiddleware(app, middlewares=middlewares)
.. dropdown:: View a detailed reference of the :class:`~connexion.ConnexionMiddleware`
:code:`__init__` method
:icon: eye
.. autoclass:: connexion.ConnexionMiddleware
:noindex:Writing custom middleware
You can add any custom middleware as long as it implements the ASGI interface. To learn how to
write pure ASGI middleware, please refer to the documentation of starlette_.
List of useful middleware
Starlette provides a bunch of useful middleware such as:
CORSMiddleware_SessionMiddleware_HTTPSRedirectMiddleware_TrustedHostMiddleware_GZipMiddleware_
Other useful middleware:
ProxyHeadersMiddleware_ by UvicornSentryASGIMiddleware_ by SentryMetricsMiddleware_ by Prometheus
For more, check the asgi-middleware topic_ on github.
.. _documentation of starlette: https://www.starlette.io/middleware/#writing-pure-asgi-middleware
.. _CORSMiddleware: https://www.starlette.io/middleware/#corsmiddleware
.. _SessionMiddleware: https://www.starlette.io/middleware/#sessionmiddleware
.. _HTTPSRedirectMiddleware: https://www.starlette.io/middleware/#httpsredirectmiddleware
.. _TrustedHostMiddleware: https://www.starlette.io/middleware/#trustedhostmiddleware
.. _GZipMiddleware: https://www.starlette.io/middleware/#gzipmiddleware
.. _ProxyHeadersMiddleware: https://github.com/encode/uvicorn/blob/master/uvicorn/middleware/proxy_headers.py
.. _SentryASGIMiddleware: https://docs.sentry.io/platforms/python/configuration/integrations/asgi/
.. _MetricsMiddleware: https://github.com/claws/aioprometheus/blob/master/src/aioprometheus/asgi/middleware.py
.. _asgi-middleware topic: https://github.com/topics/asgi-middleware
File: docs/quickstart.rst
Quickstart
Installation
You can install connexion using pip:
.. code-block:: bash
$ pip install connexionConnexion provides 'extras' with optional dependencies to unlock additional features:
- :code:
flask: Enables the :code:FlaskAppto build applications compatible with the Flask
ecosystem. - :code:
swagger-ui: Enables a Swagger UI console for your application. - :code:
uvicorn: Enables to run the your application using :code:app.run()for
development instead of using an external ASGI server.
You can install them as follows:
.. code-block:: bash
$ pip install connexion[<extra>]
$ pip install connexion[<extra1>,<extra2>].Creating your application
Connexion can be used either as a standalone application or as a middleware wrapping an existing
ASGI (or WSGI) application written using a different framework. The standalone application can be
built using either the :code:AsyncApp or :code:FlaskApp.
- The :code:
AsyncAppis a lightweight application with native asynchronous support. Use it if you
are starting a new project and have no specific reason to use one of the other options. - The :code:
FlaskAppleverages theFlaskframework, which is useful if you're migrating from
connexion 2.X or you want to leverage theFlaskecosystem. - The :code:
ConnexionMiddlewarecan be wrapped around any existing ASGI or WSGI application.
Use it if you already have an application written in a different framework and want to add
functionality provided by connexion
.. tab-set::
.. tab-item:: AsyncApp
:sync: AsyncApp
.. code-block:: python
:caption: **app.py**
from connexion import AsyncApp
app = AsyncApp(__name__)
.. dropdown:: View a detailed reference of the options accepted by the :code:`AsyncApp`
:icon: eye
.. autoclass:: connexion.AsyncApp
:noindex:
.. tab-item:: FlaskApp
:sync: FlaskApp
.. note::
To leverage the :code:`FlaskApp`, make sure you install connexion using the
:code:`flask` extra.
.. code-block:: python
:caption: **app.py**
from connexion import FlaskApp
app = FlaskApp(__name__)
.. dropdown:: View a detailed reference of the options accepted by the :code:`FlaskApp`
:icon: eye
.. autoclass:: connexion.FlaskApp
:noindex:
.. tab-item:: ConnexionMiddleware
:sync: ConnexionMiddleware
.. code-block:: python
:caption: **app.py**
from asgi_framework import App
from connexion import ConnexionMiddleware
app = App(__name__)
app = ConnexionMiddleware(app)
You can also wrap a WSGI application leveraging the :code:`a2wsgi.WSGIMiddleware`:
.. code-block:: python
:caption: **app.py**
from wsgi_framework import App
from connexion import ConnexionMiddleware
from a2wsgi import WSGIMiddleware
wsgi_app = App(__name__)
asgi_app = WSGIMiddleware(wsgi_app)
app = ConnexionMiddleware(asgi_app)
.. dropdown:: View a detailed reference of the options accepted by the
:code:`ConnexionMiddleware`
:icon: eye
.. autoclass:: connexion.ConnexionMiddleware
:noindex:Registering an API
While you can register individual routes on your application, connexion really shines when you
register an API defined by an OpenAPI (or Swagger) specification.
.. grid::
:padding: 0
.. grid-item:: **run.py**
.. code-block:: python
def post_greeting(name: str):
return f"Hello {name}", 200
app.add_api("openapi.yaml")
.. grid-item:: **openapi.yaml**
.. code-block:: yaml
openapi: "3.0.0"
info:
title: Greeting application
version: 0.0.1
paths:
/greeting/{name}:
post:
operationId: run.post_greeting
responses:
'200':
content:
text/plain:
schema:
type: string
parameters:
- name: name
in: path
required: true
schema:
type: stringThe operation described in your specification is automatically linked to your Python view function
via the :code:operationId. You can change this behavior using different :code:Resolvers, see
:doc:routing. When the endpoint is called, connexion will take care of routing, security,
request body and parameter parsing, and response serialization. All based on the specification.
You can add as many APIs as you want to a single application. The :code:add_api() method
provides a lot of configuration options. When an option is provided both to the App and the API,
the API value will take precedence.
.. dropdown:: View a detailed reference of the options accepted by the :code:add_api() method
:icon: eye
.. tab-set::
.. tab-item:: AsyncApp
:sync: AsyncApp
.. autofunction:: connexion.AsyncApp.add_api
:noindex:
.. tab-item:: FlaskApp
:sync: FlaskApp
.. autofunction:: connexion.FlaskApp.add_api
:noindex:
.. tab-item:: ConnexionMiddleware
:sync: ConnexionMiddleware
.. autofunction:: connexion.ConnexionMiddleware.add_api
:noindex:Running your application
You can run your application using an ASGI server such as uvicorn. If you defined your
:code:app in a python module called :code:run.py, you can run it as follows:
.. code-block:: bash
# assuming your application is defined as ``app`` in ``run.py``
$ uvicorn run:appOr with gunicorn (which is recommended in production).
.. code-block:: bash
# assuming your application is defined as ``app`` in ``run.py``
$ gunicorn -k uvicorn.workers.UvicornWorker run:appSee the uvicorn documentation_ for more details or check thisoverview of available ASGI servers_ for other options.
.. _overview of available ASGI servers: https://asgi.readthedocs.io/en/latest/implementations.html#servers
If you installed connexion using :code:connexion[uvicorn], you can run it using the
:code:run method. This is only recommended for development:
.. code-block:: python
app.run()To leverage automatic reloading of your application, you need to provide the application as an
import string. In most cases, this can be achieved as follows:
.. code-block:: python
from pathlib import Path
app.run(f"{Path(__file__).stem}:app").. dropdown:: View a detailed reference of the options accepted by the :code:run() method
:icon: eye
.. tab-set::
.. tab-item:: AsyncApp
:sync: AsyncApp
.. autofunction:: connexion.AsyncApp.run
:noindex:
.. tab-item:: FlaskApp
:sync: FlaskApp
.. autofunction:: connexion.FlaskApp.run
:noindex:
.. tab-item:: ConnexionMiddleware
:sync: ConnexionMiddleware
.. autofunction:: connexion.ConnexionMiddleware.run
:noindex:.. _uvicorn documentation: https://www.uvicorn.org/deployment/
The Swagger UI
If you installed connexion using the :code:swagger-ui extra, a Swagger UI is available for each
API, providing interactive documentation. By default the UI is hosted at :code:{base_path}/ui/
where :code:`base_path`` is the base path of the API.
https://{host}/{base_path}/ui/
.. image:: images/swagger_ui.png
Check :doc:swagger_ui for information on how to configure the UI.
Full App class reference
For more details on what you can do, view the complete API reference below.
.. tab-set::
.. tab-item:: AsyncApp
:sync: AsyncApp
.. dropdown:: View a detailed reference of the :code:`AsyncApp`
:icon: eye
.. autoclass:: connexion.AsyncApp
:members:
:undoc-members:
:inherited-members:
.. tab-item:: FlaskApp
:sync: FlaskApp
.. dropdown:: View a detailed reference of the :code:`FlaskApp`
:icon: eye
.. autoclass:: connexion.FlaskApp
:members:
:undoc-members:
:inherited-members:
.. tab-item:: ConnexionMiddleware
:sync: ConnexionMiddleware
.. dropdown:: View a detailed reference of the :code:`ConnexionMiddleware`
:icon: eye
.. autoclass:: connexion.ConnexionMiddleware
:members:
:undoc-members:File: docs/request.rst
Request handling
When your application receives a request, Connexion provides a lot of functionality based on your
OpenAPI spec:
- It checks the security (see :doc:
security) - It routes the request to the correct endpoint (see :doc:
routing) - It validates the body and parameters (see :doc:
validation) - It parses and passes the body and parameters to your python function
On this page, we zoom in on the final part.
Automatic parameter handling
.. tab-set::
.. tab-item:: AsyncApp
:sync: AsyncApp
Connexion automatically maps the parameters defined in your endpoint specification to the
arguments defined your associated Python function, parsing and casting values when
possible. All you need to do, is make sure the arguments of your function match the
parameters in your specification.
.. tab-item:: FlaskApp
:sync: FlaskApp
Connexion automatically maps the parameters defined in your endpoint specification to the
arguments defined your associated Python function, parsing and casting values when
possible. All you need to do, is make sure the arguments of your function match the
parameters in your specification.
.. tab-item:: ConnexionMiddleware
:sync: ConnexionMiddleware
Connexion can automatically map the parameters defined in your endpoint specification to
the arguments defined your associated Python function, parsing and casting values when
possible. All you need to do, is make sure the arguments of your function match the
parameters in your specification.
To activate this behavior when using the ``ConnexionMiddleware`` wrapping a third party
application, you can leverage the following decorators provided by Connexion:
* ``WSGIDecorator``: provides automatic parameter injection for WSGI applications. Note
that this decorator injects Werkzeug / Flask datastructures.
* ``FlaskDecorator``: provides automatic parameter injection and response serialization for
Flask applications.
* ``ASGIDecorator``: provides automatic parameter injection for ASGI applications. Note that
this decorator injects Starlette datastructures (such as UploadFile).
* ``StarletteDecorator``: provides automatic parameter injection and response serialization
for Starlette applications.
.. code-block:: python
:caption: **app.py**
from asgi_framework import App
from connexion import ConnexionMiddleware
from connexion.decorators import ASGIDecorator
@app.route("/greeting/<name>", methods=["POST"])
@ASGIDecorator()
def post_greeting(name):
...
app = App(__name__)
app = ConnexionMiddleware(app)
app.add_api("openapi.yaml")
For a full example, see our `Frameworks`_ example.For example, if you have an endpoint specified as:
.. tab-set::
.. tab-item:: OpenAPI 3
:sync: OpenAPI 3
.. code-block:: yaml
:caption: **openapi.yaml**
paths:
/foo:
get:
operationId: api.foo_get
parameters:
- name: message
in: query
schema:
type: string
required: true
.. tab-item:: Swagger 2
:sync: Swagger 2
.. code-block:: yaml
:caption: **swagger.yaml**
paths:
/foo:
get:
operationId: api.foo_get
parameters:
- name: message
in: query
type: string
required: trueAnd the view function as:
.. code-block:: python
:caption: api.py
def foo_get(message):
...Connexion will automatically identify that your view function expects an argument named message
and will pass in the value of the endpoint parameter message.
This works for both path and query parameters.
Body
The body will also be passed to your function.
.. tab-set::
.. tab-item:: OpenAPI 3
:sync: OpenAPI 3
In the OpenAPI 3 spec, the ``requestBody`` does not have a name. By default it will be
passed into your function as ``body``. You can use ``x-body-name`` in your operation to
override this name.
.. code-block:: yaml
:caption: **openapi.yaml**
paths:
/foo:
post:
operationId: api.foo_get
requestBody:
x-body-name: payload
content:
application/json:
schema:
...
.. code-block:: python
:caption: **api.py**
# Default
def foo_get(body)
...
# Based on x-body-name
def foo_get(payload)
...
.. tab-item:: Swagger 2
:sync: Swagger 2
In the Swagger 2 specification, you can define the name of your body. Connexion will pass
the body to your function using this name.
.. code-block:: yaml
:caption: **swagger.yaml**
paths:
/foo:
post:
consumes:
- application/json
parameters:
- in: body
name: payload
schema:
...
.. code-block:: python
:caption: **api.py**
def foo_get(payload)
...
Form data
`````````
In Swagger 2, form data is defined as parameters in your specification, and Connexion
passes these parameters individually:
.. code-block:: yaml
:caption: **swagger.yaml**
paths:
/foo:
post:
operationId: api.foo_get
consumes:
- application/json
parameters:
- in: formData
name: field1
type: string
- in: formData
name: field2
type: string
.. code-block:: python
:caption: **api.py**
def foo_get(field1, field2)
...Connexion will not automatically pass in the default values defined in your requestBody
definition, but you can activate this by configuring a different
:ref:RequestBodyValidator<validation:Custom validators>.
Files
Connexion extracts the files from the body and passes them into your view function separately:
.. tab-set::
.. tab-item:: OpenAPI 3
:sync: OpenAPI 3
.. code-block:: yaml
:caption: **openapi.yaml**
paths:
/foo:
post:
operationId: api.foo_get
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
.. tab-item:: Swagger 2
:sync: Swagger 2
In the Swagger 2 specification, you can define the name of your body. Connexion will pass
the body to your function using this name.
.. code-block:: yaml
:caption: **swagger.yaml**
paths:
/foo:
post:
consumes:
- application/json
parameters:
- name: file
type: file
in: formData.. tab-set::
.. tab-item:: AsyncApp
:sync: AsyncApp
If you're using the `AsyncApp`, the files are provided as `Starlette.UploadFile`_ instances.
.. code-block:: python
:caption: **api.py**
def foo_get(file)
assert isinstance(file, starlette.UploadFile)
...
.. tab-item:: FlaskApp
:sync: FlaskApp
If you're using the `FlaskApp`, the files are provided as `werkzeug.FileStorage`_ instances.
.. code-block:: python
:caption: **api.py**
def foo_get(file)
assert isinstance(file, werkzeug.FileStorage)
...When your specification defines an array of files:
.. code-block:: yaml
type: array
items:
type: string
format: binaryThey will be provided to your view function as a list.
.. tab-set::
.. tab-item:: AsyncApp
:sync: AsyncApp
.. code-block:: python
:caption: **api.py**
def foo_get(file)
assert isinstance(file, list)
assert isinstance(file[0], starlette.UploadFile)
...
.. tab-item:: FlaskApp
:sync: FlaskApp
.. code-block:: python
:caption: **api.py**
def foo_get(file)
assert isinstance(file, list)
assert isinstance(file[0], werkzeug.FileStorage)
..... _Starlette.UploadFile: https://www.starlette.io/requests/#request-files
.. _werkzeug.FileStorage: https://werkzeug.palletsprojects.com/en/3.0.x/datastructures/#werkzeug.datastructures.FileStorage
Optional arguments & Defaults
If a default value is defined for a parameter in the OpenAPI specification, Connexion will
automatically pass it in if no value was included in the request. If a default is defined in the
specification, you should not define a default in your Python function, as it will never be
triggered.
If an endpoint parameter is optional and no default is defined in the specification, you should
make sure the corresponding argument is optional in your Python function as well, by assigning a
default value:
.. code-block:: python
:caption: api.py
def foo_get(optional_argument=None)
...Missing arguments and kwargs
Connexion will inspect your function signature and only pass in the arguments that it defines. If
an argument is defined in your specification, but not in your function, Connexion will ignore it.
If you do define a **kwargs argument in your function signature, Connexion will pass in all
arguments, and the ones not explicitly defined in your signature will be collected in thekwargs argument.
Parameter Name Sanitation
The names of query and form parameters, as well as the name of the body
parameter are sanitized by removing characters that are not allowed in Python
symbols. I.e. all characters that are not letters, digits or the underscore are
removed, and finally characters are removed from the front until a letter or an
underscore is encountered. As an example:
.. code-block:: python
>>> re.sub('^[^a-zA-Z_]+', '', re.sub('[^0-9a-zA-Z_]', '', '$top'))
'top'Pythonic parameters
You can activate Pythonic parameters by setting the pythonic_params option to True on
either the application or the API:
.. tab-set::
.. tab-item:: AsyncApp
:sync: AsyncApp
.. code-block:: python
:caption: **app.py**
from connexion import AsyncApp
app = AsyncApp(__name__, pythonic_params=True)
app.add_api("openapi.yaml", pythonic_params=True)
.. tab-item:: FlaskApp
:sync: FlaskApp
.. code-block:: python
:caption: **app.py**
from connexion import FlaskApp
app = FlaskApp(__name__, pythonic_params=True)
app.add_api("openapi.yaml", pythonic_params=True):
.. tab-item:: ConnexionMiddleware
:sync: ConnexionMiddleware
.. code-block:: python
:caption: **app.py**
from asgi_framework import App
from connexion import ConnexionMiddleware
app = App(__name__)
app = ConnexionMiddleware(app, pythonic_params=True)
app.add_api("openapi.yaml", pythonic_params=True)This does two things:
- CamelCase arguments are converted to snake_case
- If the argument name matches a Python builtin, an underscore is appended.
When pythonic_params is activated, the following specification:
.. tab-set::
.. tab-item:: OpenAPI 3
:sync: OpenAPI 3
.. code-block:: yaml
:caption: **openapi.yaml**
paths:
/foo:
get:
operationId: api.foo_get
parameters:
- name: filter
in: query
schema:
type: string
required: true
- name: FilterOption
in: query
schema:
type: string
.. tab-item:: Swagger 2
:sync: Swagger 2
.. code-block:: yaml
:caption: **swagger.yaml**
paths:
/foo:
get:
operationId: api.foo_get
parameters:
- name: filter
in: query
type: string
required: true
- name: FilterOption
in: query
type: stringMaps to the following Python function:
.. code-block:: python
:caption: api.py
def foo_get(filter_, filter_option=None):
...Type casting
Whenever possible Connexion will try to parse your argument values and cast them to the correct
Python type:
+--------------+-------------+
| OpenAPI Type | Python Type |
| | |
+==============+=============+
| integer | int |
+--------------+-------------+
| string | str |
+--------------+-------------+
| number | float |
+--------------+-------------+
| boolean | bool |
+--------------+-------------+
| array | list |
+--------------+-------------+
| object | dict |
+--------------+-------------+
| null | None |
+--------------+-------------+
Parameter serialization
Array and object parameters need to be serialized into lists and dicts.
.. tab-set::
.. tab-item:: OpenAPI 3
:sync: OpenAPI 3
The `OpenAPI 3 specification`_ defines the `style` and `explode` keywords which specify how
these parameters should be serialized.
To handle these, Connexion provides the ``OpenAPIUriParser`` class, which is enabled by
default when using an OpenAPI 3 spec.
Not all combinations of `style` and `explode` are supported yet. Please open an `issue`_ if
you run into any problems.
.. tab-item:: Swagger 2
:sync: Swagger 2
The `Swagger 2 specification`_ defines the `collectionFormat` keyword to specify how
these parameters should be serialized.
To handle this for you, Connexion provides the ``Swagger2URIParser`` class, which is
enabled by default when using a Swagger 2 spec. It currently supports the `pipes`, `csv`,
and `multi` collection formats.
This parser adheres to the Swagger 2.0 spec, and will only join together multiple instance
of the same query parameter if the collectionFormat is set to `multi`. Query parameters
are parsed from left to right, so if a query parameter is defined twice, then the
right-most definition wins. For example, if you provided a URI with the query string
``?letters=a,b,c&letters=d,e,f`` and ``collectionFormat: csv``, then connexion will set
``letters = ['d', 'e', 'f']``.
Connexion also provides two alternative parsers:
* The ``FirstValueURIParser``, which behaves like the ``Swagger2URIParser``, except that it
prefers the first defined value.
* The ``AlwaysMultiURIParser``, which behaves like the ``Swagger2URIParser``, except that
it always joins together multiple instances of the same query parameter.Context
Connexion can pass in some additional context. By default, this contains the following information:
.. code-block:: python
{
"api_base_path": ... # The base path of the matched API
"operation_id": ... # The operation id of matched operation
"user": ... # User information from authentication
"token_info": ... # Token information from authentication
}Third party or custom middleware might add additional fields to this.
To receive this in your function, you can either:
Specify the
context_argument in your function signature, and the context dict will be
passed in as a whole:.. code-block:: python
:caption: api.pydef foo_get(context_):
...Specify the keys individually in your function signature:
.. code-block:: python
:caption: api.pydef foo_get(user, token_info):
...
Request object
Connexion also exposes a Request class which holds all the information about the incoming
request.
.. code-block:: python
from connexion import request.. dropdown:: View a detailed reference of the connexion.request class
:icon: eye
.. warning::
The asynchronous body arguments (body, form, files) might already be consumed by connexion.
We recommend to let Connexion inject them into your view function as mentioned above.
.. autoclass:: connexion.lifecycle.ConnexionRequest
:members:
:undoc-members:
:inherited-members:.. _Frameworks: https://github.com/spec-first/connexion/tree/main/examples/frameworks
.. _OpenAPI 3 specification: https://swagger.io/docs/specification/serialization
.. _Swagger 2 specification: https://swagger.io/docs/specification/2-0/describing-parameters/#array
.. _issue: https://github.com/spec-first/connexion/issues