Skip to content

Commit

Permalink
Add validation documentation (#1743)
Browse files Browse the repository at this point in the history
Contributes to #1531
  • Loading branch information
RobbeSneyders committed Oct 17, 2023
1 parent 41c525c commit 655ea43
Show file tree
Hide file tree
Showing 4 changed files with 347 additions and 0 deletions.
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
'sphinx.ext.autodoc',
'sphinx_copybutton',
'sphinx_design',
'sphinx.ext.autosectionlabel',
]
autosectionlabel_prefix_document = True

autoclass_content = 'both'

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Documentation
cli
request
response
validation
security
cookbook
exceptions
Expand Down
19 changes: 19 additions & 0 deletions docs/request.rst
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ The body will also be passed to your function.
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>`.

Optional arguments & Defaults
-----------------------------

Expand Down Expand Up @@ -248,6 +252,21 @@ If you do define a ``**kwargs`` argument in your function signature, Connexion w
arguments, and the ones not explicitly defined in your signature will be collected in the
``kwargs`` 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
-------------------

Expand Down
325 changes: 325 additions & 0 deletions docs/validation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
Validation
==========

One of the most powerful Connexion features is automatic validation based on your OpenAPI
specification.

Connexion validates:

- :ref:`Requests<validation:Request validation>`

- :ref:`Parameters<validation:Parameter validation>`
- :ref:`Body<validation:RequestBody validation>`
- :ref:`Headers<validation:Request headers validation>`

- :ref:`Response<validation:Response validation>`

- :ref:`Body<validation:ResponseBody validation>`
- :ref:`Headers<validation:Response headers validation>`

The validation behavior can easily be customized with :ref:`validation:Custom validators`

Request validation
------------------

Connexion will validate any incoming requests against your specification and automatically
returns the correct 4XX error on failure.

Parameter validation
````````````````````

By default, Connexion checks all the request for any parameters defined in your specification and
validates them against their definition. This includes their schema (``type``, ``format``,
``range``, ...) and whether or not they are required or whether they can be ``null``.

You can turn on ``strict_validation`` if you want Connexion to disallow any extra parameters
that are not defined in your specification. You can set it either on the application or API level:

.. tab-set::

.. tab-item:: AsyncApp
:sync: AsyncApp

.. code-block:: python
:caption: **app.py**
from connexion import AsyncApp
app = AsyncApp(__name__, strict_validation=True)
app.add_api("openapi.yaml", strict_validation=True)
.. tab-item:: FlaskApp
:sync: FlaskApp

.. code-block:: python
:caption: **app.py**
from connexion import FlaskApp
app = FlaskApp(__name__, strict_validation=True)
app.add_api("openapi.yaml", strict_validation=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, strict_validation=True)
app.add_api("openapi.yaml", strict_validation=True)
If parameter validation fails, Connexion will return a ``400 Bad Request`` response with
information on the failure in the description.

For more information on how parameters are handled in general, see
:ref:`request:Request handling`.

RequestBody validation
``````````````````````

Connexion can automatically validate a ``requestBody`` for ``json`` and ``formData`` content
types, for which it relies on `jsonschema`_. You can plug in your own validator for other content
types (see :ref:`validation:Custom validators`).

.. note::
If the ``Content-Type`` header is not set in the request, Connexion will check your
specification for which content types it accepts. If it only accepts a single content type,
Connexion assumes the request to have this content type and will validate it accordingly. If
your specification specifies no or multiple content types it accepts, Connexion will assume
the request to have content type ``application/octet-stream; charset=utf-8`` and will skip
``requestBody`` validation.

If ``requestBody`` validation fails, Connexion will return a ``400 Bad Request`` response with
information on the failure in the description.

For more information on how the ``requestBody`` is handled in general, see
:ref:`request:Body`.

Request headers validation
``````````````````````````

Headers and cookies are also validated against your specification. If their validation fails,
Connexion will return a ``400 Bad Request`` response with information on the failure in the
description.

The ``Content-Type`` header is validated separately. If it fails validation, Connexion returns a
``415 Unsupported Media Type`` error.

.. note::
If the ``Content-Type`` header is not set in the request, Connexion will make an assumption
on the content type (see :ref:`validation:RequestBody validation`) and validate it against your
spec, which might fail.

Response validation
-------------------

Connexion **will not validate outgoing responses by default** , but you can activate this by passing
the ``validate_responses`` argument to either your application or API:

.. tab-set::

.. tab-item:: AsyncApp
:sync: AsyncApp

.. code-block:: python
:caption: **app.py**
from connexion import AsyncApp
app = AsyncApp(__name__, validate_responses=True)
app.add_api("openapi.yaml", validate_responses=True)
.. tab-item:: FlaskApp
:sync: FlaskApp

.. code-block:: python
:caption: **app.py**
from connexion import FlaskApp
app = FlaskApp(__name__, validate_responses=True)
app.add_api("openapi.yaml", validate_responses=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, validate_responses=True)
app.add_api("openapi.yaml", validate_responses=True)
ResponseBody validation
```````````````````````

Connexion has built-in validators for the ``application/json`` and ``text/plain`` content types.
If the content type is not explicitly set, Connexion will infer it (see :ref:`response:Headers`),
and validate the body using the corresponding validator.

Response headers validation
```````````````````````````

Connexion will check for any required response headers that are missing and will validate the
``Content-Type`` header against the responses defined in your specification.

.. note::
If the content type is not explicitly set, Connexion will infer it
(see :ref:`response:Headers`), and validate the inferred content type, which can still fail.

Custom validators
-----------------

Connexion provides a ``validator_map`` argument which you can use to pass in custom validators.
The default validators are defined in ``connexion.validators.VALIDATOR_MAP``:

.. code-block:: python
:caption: **connexion.validators**
VALIDATOR_MAP = {
"parameter": ParameterValidator,
"body": MediaTypeDict(
{
"*/*json": JSONRequestBodyValidator,
"application/x-www-form-urlencoded": FormDataValidator,
"multipart/form-data": MultiPartFormDataValidator,
}
),
"response": MediaTypeDict(
{
"*/*json": JSONResponseBodyValidator,
"text/plain": TextResponseBodyValidator,
}
),
}
Note that the ``"body"`` and ``"response"`` values are instances of the special ``MediaTypeDict``
datastructure, which can handle Media Type ranges:

.. autoclass:: connexion.datastructures.MediaTypeDict

You can create your own custom Validator by subclassing the
``connexion.validators.AbstractRequestBodyValidator`` or
``connexion.validators.AbstractResponseBodyValidator`` class and override the defaults by passing
in a custom ``validator_map`` to your application or API:

.. code-block:: python
:caption: **app.py**
from connexion.datastructures import MediaTypeDict
from connexion.validators import AbstractResponseBodyValidator, TextResponseBodyValidator
class MyCustomXMLResponseValidator(AbstractResponseBodyValidator):
def _parse(self, stream: t.Generator[bytes, None, None]) -> t.Any:
...
def _validate(self, body: dict):
...
validator_map = {
"response": MediaTypeDict(
{
"*/*json": JSONResponseBodyValidator,
"*/*xml": MyCustomXMLResponseValidator,
"text/plain": TextResponseBodyValidator,
}
),
}
.. tab-set::

.. tab-item:: AsyncApp
:sync: AsyncApp

.. code-block:: python
:caption: **app.py**
from connexion import AsyncApp
app = AsyncApp(__name__, validator_map=validator_map)
app.add_api("openapi.yaml", validator_map=validator_map)
.. tab-item:: FlaskApp
:sync: FlaskApp

.. code-block:: python
:caption: **app.py**
from connexion import FlaskApp
app = FlaskApp(__name__, validator_map=validator_map)
app.add_api("openapi.yaml", validator_map=validator_map)
.. 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, validator_map=validator_map)
app.add_api("openapi.yaml", validator_map=validator_map)
Note that this will override the ``"response"`` section of the default ``VALIDATOR_MAP``, and
the ``"response"`` section only. This means that you need to include all ``ResponseValidators``
that you want to be active, or they will be removed.

If you want to deactivate request validation, you can pass in an empty dictionary:

.. code-block:: python
validator_map = {
"body": {}
}
Which you then pass into your application or API as mentioned above.

Inserting requestBody defaults
``````````````````````````````

You can let Connexion automatically insert default values as defined in your specification into
an incoming ``requestBody`` by configuring the ``DefaultsJSONRequestBodyValidator``:

.. code-block:: python
:caption: **app.py**
from connexion.datastructures import MediaTypeDict
from connexion.validators import (
DefaultsJSONRequestBodyValidator,
FormDataValidator,
MultiPartFormDataValidator,
)
validator_map = {
"body": MediaTypeDict(
{
"*/*json": DefaultsJSONRequestBodyValidator,
"application/x-www-form-urlencoded": FormDataValidator,
"multipart/form-data": MultiPartFormDataValidator,
}
),
}
Which you then pass into your application or API as mentioned above.

See our `enforce defaults`_ example for a full example.

.. _enforce defaults: https://github.com/spec-first/connexion/tree/main/examples/enforcedefaults
.. _jsonschema: https://github.com/python-jsonschema/jsonschema

0 comments on commit 655ea43

Please sign in to comment.