Sanic: Custom JSON encoder?

Created on 22 Aug 2017  ·  3Comments  ·  Source: sanic-org/sanic

I'm getting the following error when I call json() on a dict that contains numpy floats:

    return HTTPResponse(json_dumps(body, **kwargs), headers=headers,
OverflowError: Maximum recursion level reached

I'm porting an application from flask to sanic but am stuck there. With flask, I had the following custom JSON encoder:

class CustomJSONEncoder(JSONEncoder):
    '''
    Custom JSON encoder that converts numpy.float64 values to strings.
    '''
    def default(self, val):
        if isinstance(val, numpy.int32):
            return int(val)
        if isinstance(val, numpy.int64):
            return int(val)
        if isinstance(val, numpy.number):
            return str(val)
        if isinstance(val, numpy.ndarray):
            lst = []
            for v in val:
                lst.append(str(v))
            return lst
        return JSONEncoder.default(self, val)

I tried json(data, cls=CustomJSONEncoder) but it fails since you're using ujson which doesn't accept a custom encoder class.

Any suggestion on how to best approach this issue?

Most helpful comment

This should be possible now via https://github.com/channelcat/sanic/pull/948. Please reopen if there is an issue with it.

All 3 comments

Using the following work around:

from sanic.response import json as sanic_json

def is_iterable(x):
    try:
        iter(x)
        return True
    except TypeError:
        return False


def map_anything(x, fn):
    if isinstance(x, str):
        return fn(x)
    if isinstance(x, dict):
        return {k: map_anything(v, fn) for k, v in x.items()}
    if is_iterable(x):
        return [map_anything(ele, fn) for ele in x]
    return fn(x)


def prepare_for_json(val):
    if isinstance(val, numpy.int32):
        return int(val)
    if isinstance(val, numpy.int64):
        return int(val)
    if isinstance(val, numpy.number):
        return str(val)
    if isinstance(val, numpy.ndarray):
        return [str(v) for v in val]
    return val


def json(data, **kwargs):
    # cls not supported by ujson
    # https://github.com/esnme/ultrajson/issues/124
    return sanic_json(map_anything(data, prepare_for_json), **kwargs)

The most elegant solution to this is given in the offending line itself. You could create your own json_response, which is essentially a wrapper around HTTPResponse (just like in the first line of your exception).

So something along the lines of:

def json(body, status, headers, content_type):
    return HTTPResponse(json.dumps(body), status=status, headers=headers, content_type=content_type)

Configure and use whichever json module you want there, together with your custom encoder.

This should be possible now via https://github.com/channelcat/sanic/pull/948. Please reopen if there is an issue with it.

Was this page helpful?
0 / 5 - 0 ratings