/ python

Falcon framework - JSON middleware (loads, dumps)

If the user sends JSON to our API endpoint, we want to make a dictionary from it and allow it to use in API code. If we return a dictionary from API endpoint, we want to automatically convert it to JSON because that's how it should be returned from API.

Let's create middleware.

import json

import falcon

from core.utils import json_serializer


class JSONTranslator:

    def process_request(self, req, resp):
        """
        req.stream corresponds to the WSGI wsgi.input environ variable,
        and allows you to read bytes from the request body.
        See also: PEP 3333
        """

        if req.content_length in (None, 0):
            return

        body = req.stream.read()

        if not body:
            raise falcon.HTTPBadRequest(
                'Empty request body. A valid JSON document is required.'
            )

        try:
            req.context['request'] = json.loads(body.decode('utf-8'))
        except (ValueError, UnicodeDecodeError):
            raise falcon.HTTPError(
                falcon.HTTP_753,
                'Malformed JSON. Could not decode the request body.'
                'The JSON was incorrect or not encoded as UTF-8.'
            )

    def process_response(self, req, resp, resource, req_succeeded):
        if 'response' not in resp.context:
            return

        resp.body = json.dumps(
            resp.context['response'],
            default=json_serializer
        )

As you can see in process_request, this middleware tries to read data which was sent to API endpoint only if there is a content_length set. Next, it tries to load JSON data. If it's correct, it sets data in req.context['request'], if not, it returns HTTPError.

The process_response is used to convert the dictionary to JSON. For example, it uses the additional method, json_serializer which converts datetime and decimal to strings so if our API returns those data types, middleware (json.dumps() more precisely) will convert them to string (because datetime and decimal are not JSON serializable).

Code for json_serializers:

import datetime
import decimal


def json_serializer(obj):
    if isinstance(obj, datetime.datetime):
        return str(obj)
    elif isinstance(obj, decimal.Decimal):
        return str(obj)

    raise TypeError('Cannot serialize {!r} (type {})'.format(obj, type(obj)))

Register our middleware in Falcon application.

import falcon


app = falcon.API(middleware=[
    JSONTranslator(),
])

API example:

class MyAPI:

   def on_post(self, req, resp):
       # when a user sends a correct data it is
       # accessible in req.context['request']
       # as a python dictionary

       # if we want to set a response, we have to
       # set it in resp.context['response'], example:
       resp.context['response'] = {'foo': bar'}
       # JSONTranslator will convert dictionary to JSON and
       # set it in resp.body which contains data which will
       # be returned as an API response

This middleware is just a modified copy of middleware from Falcon documentation because I want to have a note with description how it works and how to use it.