How to serialize numpy.float32 (and other types) to JSON

Aug 10, 2018 · 343 words · 2 minutes read jsonnumpypythonsingle dispatch

Back in 2016 I wrote about how numpy.float64 is JSON serializable but numpy.float32 is not. Since then I’ve learned a much better way to seralize numpy.float32 (and other types).

In my other post I noted that you can convert numpy.float32 back to numpy.float64 (which is JSON serializable) simply by multiplying it. But who wants to do this every time? What if your array sometimes contains numpy.float64 and sometimes contains numpy.float32? The trick is to use single dispatch.

The singledispatch function was added to functools in Python 3.4 to define a generic function. Generic functions are a composition of multiple functions, corrsponding to the argument type.

Without singledistpatch you might write something like this:

if isinstance(var, np.float32):
    # case when var is numpy.float32
elif isinstance(var, np.float64):
    # case when var is numpy.float64
else:
    # case when var is neither

However using single dispatch you define one function for each type (e.g. float32, float64, etc.) plus a default function if the argument doesn’t match any given type.

#!/usr/local/bin/python3
import numpy as np
import json
from functools import singledispatch


@singledispatch
def to_serializable(val):
    """Used by default."""
    return str(val)


@to_serializable.register(np.float32)
def ts_float32(val):
    """Used if *val* is an instance of numpy.float32."""
    return np.float64(val)


json.dumps({'pi': np.float32(3.1415)}, default=to_serializable)

When the value is numpy.float32 then to_serialiazble uses the ts_float32 function and otherwise uses the default function (itself).

The great thing about this approach is that there are a lot of different ways you can use single dispatch to help with JSON serialization, such as formatting datetime objects:

@to_serializable.register(datetime.datetime)
def ts_datetime(val):
    """Used if *val* is an instance of datetime.datetime."""
    return val.isoformat() + "Z"

You can define serialization methods for any type by using the @to_serializable.register decorator.

Note that to_serializable was the name of the function we define for the default case – you can give this function another name and use that in the decorator name for your single dispatch functions. You could also define multiple serializers for different cases. For example you could have one serializer convert datetimes to ISO 8601 formatted strings and another serializer that converts them to unix time.