How to make a class JSON serializable

Viewed 1213094

How to make a Python class serializable?

class FileItem:
    def __init__(self, fname):
        self.fname = fname

Attempt to serialize to JSON:

>>> import json
>>> x = FileItem('/foo/bar')
>>> json.dumps(x)
TypeError: Object of type 'FileItem' is not JSON serializable
39 Answers

As mentioned in many other answers you can pass a function to json.dumps to convert objects that are not one of the types supported by default to a supported type. Surprisingly none of them mentions the simplest case, which is to use the built-in function vars to convert objects into a dict containing all their attributes:

json.dumps(obj, default=vars)

Note that this covers only basic cases, if you need more specific serialization for certain types (e.g. exluding certain attributes or for objects that don't have a __dict__ attribute) you need to use a custom function or a JSONEncoder as desribed in the other answers.

If you're using Python3.5+, you could use jsons. (PyPi: https://pypi.org/project/jsons/) It will convert your object (and all its attributes recursively) to a dict.

import jsons

a_dict = jsons.dump(your_object)

Or if you wanted a string:

a_str = jsons.dumps(your_object)

Or if your class implemented jsons.JsonSerializable:

a_dict = your_object.json

The Real Answer to:
Making Pythons json module work with Your Class

AKA, solving: json.dumps({ "thing": YOUR_CLASS() })


TLDR: copy-paste Option 1 or Option 2 below

Explanation:

  • Yes, a good reliable solution exists
  • No, there is no python "official" solution
    • By official solution, I mean there is no way (as of 2022) to add a method to your class (like toJSON in JavaScript) and/or no way to register your class with the built-in json module. When something like json.dumps([1,2, your_obj]) is executed, python doesn't check a lookup table or object method.
    • I'm not sure why other answers don't explain this
    • The closest official approach is probably andyhasit's answer which is to inherit from a dictionary. However, inheriting from a dictionary doesn't work very well for many custom classes like AdvancedDateTime, or pytorch tensors.
  • The ideal workaround is this:
    • Mutate json.dumps (affects everywhere, even pip modules that import json)
    • Add def __json__(self) method to your class

Option 1: Let a Module do the Patching


pip install json-fix
(extended + packaged version of Fancy John's answer, thank you @FancyJohn)

your_class_definition.py

import json_fix

class YOUR_CLASS:
    def __json__(self):
        # YOUR CUSTOM CODE HERE
        #    you probably just want to do:
        #        return self.__dict__
        return "a built-in object that is naturally json-able"

Thats it.


Example usage:

from your_class_definition import YOUR_CLASS
import json

json.dumps([1,2, YOUR_CLASS()], indent=0)
# '[\n1,\n2,\n"a built-in object that is naturally json-able"\n]'

To make json.dumps work for Numpy arrays, Pandas DataFrames, and other 3rd party objects, see the Module (only ~2 lines of code but needs explanation).




How does it work? Well...

Option 2: Patch json.dumps yourself


Note: this approach is simplified, it fails on known edgecases (ex: if your custom class inherits from dict or another builtin), and it misses out on controlling the json behavior for external classes (numpy arrays, datetime, dataframes, tensors, etc).

some_file_thats_imported_before_your_class_definitions.py

# Step: 1
# create the patch
from json import JSONEncoder
def wrapped_default(self, obj):
    return getattr(obj.__class__, "__json__", wrapped_default.default)(obj)
wrapped_default.default = JSONEncoder().default
   
# apply the patch
JSONEncoder.original_default = JSONEncoder.default
JSONEncoder.default = wrapped_default

your_class_definition.py

# Step 2
class YOUR_CLASS:
    def __json__(self, **options):
        # YOUR CUSTOM CODE HERE
        #    you probably just want to do:
        #        return self.__dict__
        return "a built-in object that is natually json-able"

_

All other answers seem to be "Best practices/approaches to serializing a custom object"

Which, is alreadly covered here in the docs (search "complex" for an example of encoding complex numbers)

This class can do the trick, it converts object to standard json .

import json


class Serializer(object):
    @staticmethod
    def serialize(object):
        return json.dumps(object, default=lambda o: o.__dict__.values()[0])

usage:

Serializer.serialize(my_object)

working in python2.7 and python3.

jaraco gave a pretty neat answer. I needed to fix some minor things, but this works:

Code

# Your custom class
class MyCustom(object):
    def __json__(self):
        return {
            'a': self.a,
            'b': self.b,
            '__python__': 'mymodule.submodule:MyCustom.from_json',
        }

    to_json = __json__  # supported by simplejson

    @classmethod
    def from_json(cls, json):
        obj = cls()
        obj.a = json['a']
        obj.b = json['b']
        return obj

# Dumping and loading
import simplejson

obj = MyCustom()
obj.a = 3
obj.b = 4

json = simplejson.dumps(obj, for_json=True)

# Two-step loading
obj2_dict = simplejson.loads(json)
obj2 = MyCustom.from_json(obj2_dict)

# Make sure we have the correct thing
assert isinstance(obj2, MyCustom)
assert obj2.__dict__ == obj.__dict__

Note that we need two steps for loading. For now, the __python__ property is not used.

How common is this?

Using the method of AlJohri, I check popularity of approaches:

Serialization (Python -> JSON):

Deserialization (JSON -> Python):

This has worked well for me:

class JsonSerializable(object):

    def serialize(self):
        return json.dumps(self.__dict__)

    def __repr__(self):
        return self.serialize()

    @staticmethod
    def dumper(obj):
        if "serialize" in dir(obj):
            return obj.serialize()

        return obj.__dict__

and then

class FileItem(JsonSerializable):
    ...

and

log.debug(json.dumps(<my object>, default=JsonSerializable.dumper, indent=2))

Kyle Delaney's comment is correct so i tried to use the answer https://stackoverflow.com/a/15538391/1497139 as well as an improved version of https://stackoverflow.com/a/10254820/1497139

to create a "JSONAble" mixin.

So to make a class JSON serializeable use "JSONAble" as a super class and either call:

 instance.toJSON()

or

 instance.asJSON()

for the two offered methods. You could also extend the JSONAble class with other approaches offered here.

The test example for the Unit Test with Family and Person sample results in:

toJSOn():

{
    "members": {
        "Flintstone,Fred": {
            "firstName": "Fred",
            "lastName": "Flintstone"
        },
        "Flintstone,Wilma": {
            "firstName": "Wilma",
            "lastName": "Flintstone"
        }
    },
    "name": "The Flintstones"
}

asJSOn():

{'name': 'The Flintstones', 'members': {'Flintstone,Fred': {'firstName': 'Fred', 'lastName': 'Flintstone'}, 'Flintstone,Wilma': {'firstName': 'Wilma', 'lastName': 'Flintstone'}}}

Unit Test with Family and Person sample

def testJsonAble(self):
        family=Family("The Flintstones")
        family.add(Person("Fred","Flintstone")) 
        family.add(Person("Wilma","Flintstone"))
        json1=family.toJSON()
        json2=family.asJSON()
        print(json1)
        print(json2)

class Family(JSONAble):
    def __init__(self,name):
        self.name=name
        self.members={}
    
    def add(self,person):
        self.members[person.lastName+","+person.firstName]=person

class Person(JSONAble):
    def __init__(self,firstName,lastName):
        self.firstName=firstName;
        self.lastName=lastName;

jsonable.py defining JSONAble mixin

 '''
Created on 2020-09-03

@author: wf
'''
import json

class JSONAble(object):
    '''
    mixin to allow classes to be JSON serializable see
    https://stackoverflow.com/questions/3768895/how-to-make-a-class-json-serializable
    '''

    def __init__(self):
        '''
        Constructor
        '''
    
    def toJSON(self):
        return json.dumps(self, default=lambda o: o.__dict__, 
            sort_keys=True, indent=4)
        
    def getValue(self,v):
        if (hasattr(v, "asJSON")):
            return v.asJSON()
        elif type(v) is dict:
            return self.reprDict(v)
        elif type(v) is list:
            vlist=[]
            for vitem in v:
                vlist.append(self.getValue(vitem))
            return vlist
        else:   
            return v
    
    def reprDict(self,srcDict):
        '''
        get my dict elements
        '''
        d = dict()
        for a, v in srcDict.items():
            d[a]=self.getValue(v)
        return d
    
    def asJSON(self):
        '''
        recursively return my dict elements
        '''
        return self.reprDict(self.__dict__)   

You'll find these approaches now integrated in the https://github.com/WolfgangFahl/pyLoDStorage project which is available at https://pypi.org/project/pylodstorage/

Why are you guys making it so complicated? Here is a simple example:

#!/usr/bin/env python3

import json
from dataclasses import dataclass

@dataclass
class Person:
    first: str
    last: str
    age: int

    @property
    def __json__(self):
        return {
            "name": f"{self.first} {self.last}",
            "age": self.age
        }

john = Person("John", "Doe", 42)
print(json.dumps(john, indent=4, default=lambda x: x.__json__))

This way you could also serialize nested classes, as __json__ returns a python object and not a string. No need to use a JSONEncoder, as the default parameter with a simple lambda also works fine.

I've used @property instead of a simple function, as this feels more natural and modern. The @dataclass is also just an example, it works for a "normal" class as well.

Building on Quinten Cabo's answer:

def sterilize(obj):
    """Make an object more ameniable to dumping as json
    """
    if type(obj) in (str, float, int, bool, type(None)):
        return obj
    elif isinstance(obj, dict):
        return {k: sterilize(v) for k, v in obj.items()}
    list_ret = []
    dict_ret = {}
    for a in dir(obj):
        if a == '__iter__' and callable(obj.__iter__):
            list_ret.extend([sterilize(v) for v in obj])
        elif a == '__dict__':
            dict_ret.update({k: sterilize(v) for k, v in obj.__dict__.items() if k not in ['__module__', '__dict__', '__weakref__', '__doc__']})
        elif a not in ['__doc__', '__module__']:
            aval = getattr(obj, a)
            if type(aval) in (str, float, int, bool, type(None)):
                dict_ret[a] = aval
            elif a != '__class__' and a != '__objclass__' and isinstance(aval, type):
                dict_ret[a] = sterilize(aval)
    if len(list_ret) == 0:
        if len(dict_ret) == 0:
            return repr(obj)
        return dict_ret
    else:
        if len(dict_ret) == 0:
            return list_ret
    return (list_ret, dict_ret)

The differences are

  1. Works for any iterable instead of just list and tuple (it works for NumPy arrays, etc.)
  2. Works for dynamic types (ones that contain a __dict__).
  3. Includes native types float and None so they don't get converted to string.
  4. Classes that have __dict__ and members will mostly work (if the __dict__ and member names collide, you will only get one - likely the member)
  5. Classes that are lists and have members will look like a tuple of the list and a dictionary
  6. Python3 (that isinstance() call may be the only thing that needs changing)
class DObject(json.JSONEncoder):
    def delete_not_related_keys(self, _dict):
        for key in ["skipkeys", "ensure_ascii", "check_circular", "allow_nan", "sort_keys", "indent"]:
            try:
                del _dict[key]
            except:
                continue

    def default(self, o):
        if hasattr(o, '__dict__'):
            my_dict = o.__dict__.copy()
            self.delete_not_related_keys(my_dict)
            return my_dict
        else:
            return o

a = DObject()
a.name = 'abdul wahid'
b = DObject()
b.name = a

print(json.dumps(b, cls=DObject))

To throw another log on this 11 year old fire, I want a solution that meets the following criteria:

  • Allows an instance of class FileItem to be serialized using only json.dumps(obj)
  • Allows FileItem instances to have properties: fileItem.fname
  • Allows FileItem instances to be given to any library which will serialise it using json.dumps(obj)
  • Doesn't require any other fields to be passed to json.dumps (like a custom serializer)

IE:

fileItem = FileItem('filename.ext')
assert json.dumps(fileItem) == '{"fname": "filename.ext"}'
assert fileItem.fname == 'filename.ext'

My solution is:

  • Have obj's class inherit from dict
  • Map each object property to the underlying dict
class FileItem(dict):
    def __init__(self, fname):
        self['fname'] = fname

    #fname property
    fname: str = property()
    @fname.getter
    def fname(self):
        return self['fname']

    @fname.setter
    def fname(self, value: str):
        self['fname'] = value

    #Repeat for other properties

Yes, this is somewhat long winded if you have lots of properties, but it is JSONSerializable and it behaves like an object and you can give it to any library that's going to json.dumps(obj) it.

I liked Lost Koder's method the most. I ran into issues when trying to serialize more complex objects whos members/methods aren't serializable. Here's my implementation that works on more objects:

class Serializer(object):
    @staticmethod
    def serialize(obj):
        def check(o):
            for k, v in o.__dict__.items():
                try:
                    _ = json.dumps(v)
                    o.__dict__[k] = v
                except TypeError:
                    o.__dict__[k] = str(v)
            return o
        return json.dumps(check(obj).__dict__, indent=2)

I ran into this problem when I tried to store Peewee's model into PostgreSQL JSONField.

After struggling for a while, here's the general solution.

The key to my solution is going through Python's source code and realizing that the code documentation (described here) already explains how to extend the existing json.dumps to support other data types.

Suppose you current have a model that contains some fields that are not serializable to JSON and the model that contains the JSON field originally looks like this:

class SomeClass(Model):
    json_field = JSONField()

Just define a custom JSONEncoder like this:

class CustomJsonEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, SomeTypeUnsupportedByJsonDumps):
            return < whatever value you want >
        return json.JSONEncoder.default(self, obj)

    @staticmethod
    def json_dumper(obj):
        return json.dumps(obj, cls=CustomJsonEncoder)

And then just use it in your JSONField like below:

class SomeClass(Model):
    json_field = JSONField(dumps=CustomJsonEncoder.json_dumper)

The key is the default(self, obj) method above. For every single ... is not JSON serializable complaint you receive from Python, just add code to handle the unserializable-to-JSON type (such as Enum or datetime)

For example, here's how I support a class inheriting from Enum:

class TransactionType(Enum):
   CURRENT = 1
   STACKED = 2

   def default(self, obj):
       if isinstance(obj, TransactionType):
           return obj.value
       return json.JSONEncoder.default(self, obj)

Finally, with the code implemented like above, you can just convert any Peewee models to be a JSON-seriazable object like below:

peewee_model = WhateverPeeweeModel()
new_model = SomeClass()
new_model.json_field = model_to_dict(peewee_model)

Though the code above was (somewhat) specific to Peewee, but I think:

  1. It's applicable to other ORMs (Django, etc) in general
  2. Also, if you understood how json.dumps works, this solution also works with Python (sans ORM) in general too

Any questions, please post in the comments section. Thanks!

First we need to make our object JSON-compliant, so we can dump it using the standard JSON module. I did it this way:

def serialize(o):
    if isinstance(o, dict):
        return {k:serialize(v) for k,v in o.items()}
    if isinstance(o, list):
        return [serialize(e) for e in o]
    if isinstance(o, bytes):
        return o.decode("utf-8")
    return o

This function uses recursion to iterate over every part of the dictionary and then calls the repr() methods of classes that are not build-in types.

def sterilize(obj):
    object_type = type(obj)
    if isinstance(obj, dict):
        return {k: sterilize(v) for k, v in obj.items()}
    elif object_type in (list, tuple):
        return [sterilize(v) for v in obj]
    elif object_type in (str, int, bool, float):
        return obj
    else:
        return obj.__repr__()

Whomever wants to use basic conversion without an external library, it is simply how you can override __iter__ & __str__ functions of the custom class using following way.

class JSONCustomEncoder(json.JSONEncoder):
    def default(self, obj):
        return obj.__dict__


class Student:
    def __init__(self, name: str, slug: str):
        self.name = name
        self.age = age

    def __iter__(self):
        yield from {
            "name": self.name,
            "age": self.age,
        }.items()

    def __str__(self):
        return json.dumps(
            self.__dict__, cls=JSONCustomEncoder, ensure_ascii=False
        )

Use the object by wrapping in a dict(), so that data remains preserved.

s = Student("aman", 24)
dict(s)

If you are able to install a package, I'd recommend trying dill, which worked just fine for my project. A nice thing about this package is that it has the same interface as pickle, so if you have already been using pickle in your project you can simply substitute in dill and see if the script runs, without changing any code. So it is a very cheap solution to try!

(Full anti-disclosure: I am in no way affiliated with and have never contributed to the dill project.)

Install the package:

pip install dill

Then edit your code to import dill instead of pickle:

# import pickle
import dill as pickle

Run your script and see if it works. (If it does you may want to clean up your code so that you are no longer shadowing the pickle module name!)

Some specifics on datatypes that dill can and cannot serialize, from the project page:

dill can pickle the following standard types:

none, type, bool, int, long, float, complex, str, unicode, tuple, list, dict, file, buffer, builtin, both old and new style classes, instances of old and new style classes, set, frozenset, array, functions, exceptions

dill can also pickle more ‘exotic’ standard types:

functions with yields, nested functions, lambdas, cell, method, unboundmethod, module, code, methodwrapper, dictproxy, methoddescriptor, getsetdescriptor, memberdescriptor, wrapperdescriptor, xrange, slice, notimplemented, ellipsis, quit

dill cannot yet pickle these standard types:

frame, generator, traceback

To add another option: You can use the attrs package and the asdict method.

class ObjectEncoder(JSONEncoder):
    def default(self, o):
        return attr.asdict(o)

json.dumps(objects, cls=ObjectEncoder)

and to convert back

def from_json(o):
    if '_obj_name' in o:
        type_ = o['_obj_name']
        del o['_obj_name']
        return globals()[type_](**o)
    else:
        return o

data = JSONDecoder(object_hook=from_json).decode(data)

class looks like this

@attr.s
class Foo(object):
    x = attr.ib()
    _obj_name = attr.ib(init=False, default='Foo')

In addition to the Onur's answer, You possibly want to deal with datetime type like below.
(in order to handle: 'datetime.datetime' object has no attribute 'dict' exception.)

def datetime_option(value):
    if isinstance(value, datetime.date):
        return value.timestamp()
    else:
        return value.__dict__

Usage:

def toJSON(self):
    return json.dumps(self, default=datetime_option, sort_keys=True, indent=4)

To throw yet another log into a 10-year old fire, I would also offer the dataclass-wizard for this task, assuming you're using Python 3.6+. This works well with dataclasses, which is actually a python builtin module in 3.7+ onwards.

The dataclass-wizard library will convert your object (and all its attributes recursively) to a dict, and makes the reverse (de-serialization) pretty straightforward too, with fromdict. Also, here is the PyPi link: https://pypi.org/project/dataclass-wizard/.

import dataclass_wizard
import dataclasses

@dataclasses.dataclass
class A:
    hello: str
    a_field: int

obj = A('world', 123)
a_dict = dataclass_wizard.asdict(obj)
# {'hello': 'world', 'aField': 123}

Or if you wanted a string:

a_str = jsons.dumps(dataclass_wizard.asdict(obj))

Or if your class extended from dataclass_wizard.JSONWizard:

a_str = your_object.to_json()

Finally, the library also supports dataclasses in Union types, which basically means that a dict can be de-serialized into an object of either class C1 or C2. For example:

from dataclasses import dataclass

from dataclass_wizard import JSONWizard

@dataclass
class Outer(JSONWizard):

    class _(JSONWizard.Meta):
        tag_key = 'tag'
        auto_assign_tags = True

    my_string: str
    inner: 'A | B'  # alternate syntax: `inner: typing.Union['A', 'B']`

@dataclass
class A:
    my_field: int

@dataclass
class B:
    my_field: str


my_dict = {'myString': 'test', 'inner': {'tag': 'B', 'myField': 'test'}}
obj = Outer.from_dict(my_dict)

# True
assert repr(obj) == "Outer(my_string='test', inner=B(my_field='test'))"

obj.to_json()
# {"myString": "test", "inner": {"myField": "test", "tag": "B"}}

There are many approaches to this problem. 'ObjDict' (pip install objdict) is another. There is an emphasis on providing javascript like objects which can also act like dictionaries to best handle data loaded from JSON, but there are other features which can be useful as well. This provides another alternative solution to the original problem.

Related