ChatGPT解决这个技术问题 Extra ChatGPT

How to make a class JSON serializable

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
It's unfortunate that the answers all seem to answer the question "How do I serialize a class?" rather than the action question "How do I make a class serializable?" These answers assume that you're doing the serialization yourself, rather than passing the object along to some other module that serializes it.
If you're using Python3.5+, you could use jsons. It will convert your object (and all its attributes recursively) to a dict. import jsons see answer below - it works perfectly fine
@KyleDelaney I was really hoping for an interface/magic method I could implement to become searializable too. I guess I will have to implement a .to_dict() function or something which can be called on the object before it is passed to the module which tries to serialize it.
It's amazing that in 11 years there has not been a single response that answers this question. OP states he wants to use json.dumps yet all the answers, including with the bounty awarded, involve creating a custom encoder, which dodges the point of the question entirely.
That said, this question serves as a canonical now, so it's entirely reasonable that it attracts answers that tell beginners the right thing(s) to do.

O
Onur Yıldırım

Here is a simple solution for a simple feature:

.toJSON() Method

Instead of a JSON serializable class, implement a serializer method:

import json

class Object:
    def toJSON(self):
        return json.dumps(self, default=lambda o: o.__dict__, 
            sort_keys=True, indent=4)

So you just call it to serialize:

me = Object()
me.name = "Onur"
me.age = 35
me.dog = Object()
me.dog.name = "Apollo"

print(me.toJSON())

will output:

{
    "age": 35,
    "dog": {
        "name": "Apollo"
    },
    "name": "Onur"
}

Very limited. If you have a dict {"foo":"bar","baz":"bat"}, that will serialize to JSON easily. If instead you have {"foo":"bar","baz":MyObject()}, then you cannot. The ideal situation would be that nested objects are serialized to JSON recursively, not explicitly.
It will still work. You're missing o.__dict___. Try your own example: class MyObject(): def __init__(self): self.prop = 1 j = json.dumps({ "foo": "bar", "baz": MyObject() }, default=lambda o: o.__dict__)
Is this solution reversible? I.e. Is it easy to reconstruct the object from json?
This does not work with datetime.datetime instances. It throws the following error: 'datetime.datetime' object has no attribute '__dict__'
I must be missing something but that seems like it doesn't work (ie., json.dumps(me) doesn't call Object's toJSON method.
m
mkrieger1

Do you have an idea about the expected output? For example, will this do?

>>> f  = FileItem("/foo/bar")
>>> magic(f)
'{"fname": "/foo/bar"}'

In that case you can merely call json.dumps(f.__dict__).

If you want more customized output then you will have to subclass JSONEncoder and implement your own custom serialization.

For a trivial example, see below.

>>> from json import JSONEncoder
>>> class MyEncoder(JSONEncoder):
        def default(self, o):
            return o.__dict__    

>>> MyEncoder().encode(f)
'{"fname": "/foo/bar"}'

Then you pass this class into the json.dumps() method as cls kwarg:

json.dumps(cls=MyEncoder)

If you also want to decode then you'll have to supply a custom object_hook to the JSONDecoder class. For example:

>>> def from_json(json_object):
        if 'fname' in json_object:
            return FileItem(json_object['fname'])
>>> f = JSONDecoder(object_hook = from_json).decode('{"fname": "/foo/bar"}')
>>> f
<__main__.FileItem object at 0x9337fac>
>>> 

Using __dict__ will not work in all cases. If the attributes have not been set after the object was instantiated, __dict__ may not be fully populated. In the example above, you're OK, but if you have class attributes that you also want to encode, those will not be listed in __dict__ unless they have been modified in the class' __init__ call or by some other way after the object was instantiated.
+1, but the from_json() function used as object-hook should have an else: return json_object statement, so it can deal with general objects as well.
@KrisHardy __dict__ also doesn't work if you use __slots__ on a new style class.
You could use a custom JSONEncoder as above to create a custom protocol, such as checking for the existence of __json_serializable__ method and calling it to obtain a JSON serializable representation of the object. This would be in keeping with other Python patterns, like __getitem__, __str__, __eq__, and __len__.
__dict__ also won't work recursively, e.g., if an attribute of your object is another object.
m
mrnom

For more complex classes you could consider the tool jsonpickle:

jsonpickle is a Python library for serialization and deserialization of complex Python objects to and from JSON. The standard Python libraries for encoding Python into JSON, such as the stdlib’s json, simplejson, and demjson, can only handle Python primitives that have a direct JSON equivalent (e.g. dicts, lists, strings, ints, etc.). jsonpickle builds on top of these libraries and allows more complex data structures to be serialized to JSON. jsonpickle is highly configurable and extendable–allowing the user to choose the JSON backend and add additional backends.

(link to jsonpickle on PyPi)


Coming from C#, this is what I was expecting. A simple one liner and no messing with the classes.
jsonpickle is awesome. It worked perfectly for a huge, complex, messy object with many levels of classes
is there an example of the proper way to save this to a file? The documentation only shows how to encode and decode a jsonpickle object. Also, this was not able to decode a dict of dicts containing pandas dataframes.
@user5359531 you can use obj = jsonpickle.decode(file.read()) and file.write(jsonpickle.encode(obj)).
It works for me!. It is what I needed. I just wanted to print a behave scenario object.
a
andyhasit

Most of the answers involve changing the call to json.dumps(), which is not always possible or desirable (it may happen inside a framework component for example).

If you want to be able to call json.dumps(obj) as is, then a simple solution is inheriting from dict:

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

f = FileItem('tasks.txt')
json.dumps(f)  #No need to change anything here

This works if your class is just basic data representation, for trickier things you can always set keys explicitly.


This can really be a nice solution :) I believe for my case it is. Benefits: you communicate the "shape" of the object by making it a class with init, it is inherently serializable and it looks interpretable as repr.
Though "dot-access" is still missing :(
Ahh that seems to work! Thanks, not sure why this is not the accepted answer. I totally agree that changing the dumps is not a good solution. By the way, in most cases you probably want to have dict inheritance together with delegation, which means that you will have some dict type attribute inside your class, you will then pass this attribute as parameter as initialisation something like super().__init__(self.elements).
In my use case I needed to store data that was "invisible" to json.dumps(), so I used this method. The class DictWithRider takes in an arbitrary object, stores it as a member, and makes it accessible via a function get_rider_obj() but does not pass it to dict.__init__(). So parts of the application who want to see the "hidden" data can call d.get_rider_obj() but json.dumps() sees basically an empty dict. As @PascalVKooten mentioned, you can't access regular members with dot notation, but you can access functions.
this solution's a bit hacky - for a true, production quality solution, replace json.dumps() and json.loads() with jsonpickle.encode() and jsonpickle.decode(). You will avoid having to write ugly boilerplate code, and most importantly, if you are able to pickle the object, you should be able to serialize it with jsonpickle without boilerplate code (complex containers/objects will just work).
u
user1587520

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.


it is unclear what you mean by default=vars, does that mean that vars is the default serializer? If not: This does not really solve the case where you can not influence how json.dumps is called. If you simply pass an object to a library and that library calls json.dumps on that object, it doesn't really help that you have implemented vars if that library does not use dumps this way. In that sense it is equivalent to a custom JSONEncoder.
You are correct, it is nothing else than just a simple choice for a custom serializer and doesn't solve the case you describe. If I see it correctly there is no solution to the case were you don't control how json.dumps is invoked.
For some objects, this approach will throw vars() argument must have __dict__ attribute
this is probably the best solution, least intrusive, and easiest to understand
Thanks for this, pretty straightforward to use with library that have proper definition built in.
C
Cadoiz

Just add to_json method to your class like this:

def to_json(self):
  return self.message # or how you want it to be serialized

And add this code (from this answer), to somewhere at the top of everything:

from json import JSONEncoder

def _default(self, obj):
    return getattr(obj.__class__, "to_json", _default.default)(obj)

_default.default = JSONEncoder().default
JSONEncoder.default = _default

This will monkey-patch json module when it's imported, so JSONEncoder.default() automatically checks for a special to_json() method and uses it to encode the object if found.

Just like Onur said, but this time you don't have to update every json.dumps() in your project.


Big thanks! This is the only answer that allows me to do what I want: be able to serialize an object without changing the existing code. The other methods mostly do not work for me. The object is defined in a third-party library, and the serialization code is third-party too. Changing them will be awkward. With your method, I only need to do TheObject.to_json = my_serializer.
This is the correct answer. I did a small variation: import json _fallback = json._default_encoder.default json._default_encoder.default = lambda obj: getattr(obj.__class__, "to_json", _fallback)(obj)
C
Community

I like Onur's answer but would expand to include an optional toJSON() method for objects to serialize themselves:

def dumper(obj):
    try:
        return obj.toJSON()
    except:
        return obj.__dict__
print json.dumps(some_big_object, default=dumper, indent=2)

I actually really like this; but rather than try-catch would probably do something like if 'toJSON' in obj.__attrs__():... to avoid a silent failure (in the event of failure in toJSON() for some other reason than it not being there)... a failure which potentially leads to data corruption.
@thclark as I understand it, idomatic python asks for forgiveness, not permission, so try-except is the right approach, but the correct exception should be caught, an AttributeError in this case.
@phil a few years older and wiser now, I'd agree with you.
This really should be catching an AttributeError explicitly
And what if AttributeError is raised inside obj.toJSON()?
u
user222758

Another option is to wrap JSON dumping in its own class:

import json

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

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

Or, even better, subclassing FileItem class from a JsonSerializable class:

import json

class JsonSerializable(object):
    def toJson(self):
        return json.dumps(self.__dict__)

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


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

Testing:

>>> f = FileItem('/foo/bar')
>>> f.toJson()
'{"fname": "/foo/bar"}'
>>> f
'{"fname": "/foo/bar"}'
>>> str(f) # string coercion
'{"fname": "/foo/bar"}'

Hi, I don't really like this "custom encoder" approach, it would be better if u can make your class json seriazable. I try, and try and try and nothing. Is there any idea how to do this. The thing is that json module test your class against built in python types, and even says for custom classes make your encoder :). Can it be faked? So I could do something to my class so it behave like simple list to json module? I try subclasscheck and instancecheck but nothing.
@ADRENALIN You could inherit from a primary type (probably dict), if all class attribute values are serializable and you don't mind hacks. You could also use jsonpickle or json_tricks or something instead of the standard one (still a custom encoder, but not one you need to write or call). The former pickles the instance, the latter stores it as dict of attributes, which you can change by implementing __json__encode__ / __json_decode__ (disclosure: I made the last one).
That doesn't make the object serializeable for the json class. It only provides a method to get a json string returned (trivial). Thus json.dumps(f) will fail. That's not what's been asked.
S
SpaceCityCowboy88

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

If you are able to use Python 3.7+, I found that the cleanest solution to convert python classes to dicts and JSON strings (and viceversa) is to mix the jsons library with dataclasses. So far, so good for me!
This is an external library, not built into the standard Python install.
only for class that has slots attribute
You can, but you don't need to use slots. Only when dumping according to the signature of a specific class you'll need slots. In the upcoming version 1.1.0 that is also no longer the case.
This library is extremely slow in both deserialization/serialization, at least from personal testing. I'd suggest other ser libraries instead.
n
np_6

I came across this problem the other day and implemented a more general version of an Encoder for Python objects that can handle nested objects and inherited fields:

import json
import inspect

class ObjectEncoder(json.JSONEncoder):
    def default(self, obj):
        if hasattr(obj, "to_json"):
            return self.default(obj.to_json())
        elif hasattr(obj, "__dict__"):
            d = dict(
                (key, value)
                for key, value in inspect.getmembers(obj)
                if not key.startswith("__")
                and not inspect.isabstract(value)
                and not inspect.isbuiltin(value)
                and not inspect.isfunction(value)
                and not inspect.isgenerator(value)
                and not inspect.isgeneratorfunction(value)
                and not inspect.ismethod(value)
                and not inspect.ismethoddescriptor(value)
                and not inspect.isroutine(value)
            )
            return self.default(d)
        return obj

Example:

class C(object):
    c = "NO"
    def to_json(self):
        return {"c": "YES"}

class B(object):
    b = "B"
    i = "I"
    def __init__(self, y):
        self.y = y
        
    def f(self):
        print "f"

class A(B):
    a = "A"
    def __init__(self):
        self.b = [{"ab": B("y")}]
        self.c = C()

print json.dumps(A(), cls=ObjectEncoder, indent=2, sort_keys=True)

Result:

{
  "a": "A", 
  "b": [
    {
      "ab": {
        "b": "B", 
        "i": "I", 
        "y": "y"
      }
    }
  ], 
  "c": {
    "c": "YES"
  }, 
  "i": "I"
}

Although this is a bit old..I'm facing some circular imports error. So instead of return obj in the last line I did this return super(ObjectEncoder, self).default(obj). Reference HERE
J
Jeff Hykin

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.

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

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]'

How does it work? See option 2 for doing it yourself.
NOTE:
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).

Option 2: Patch json.dumps yourself

Note: this approach is simplified, and 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)


j
jtlz2
import simplejson

class User(object):
    def __init__(self, name, mail):
        self.name = name
        self.mail = mail

    def _asdict(self):
        return self.__dict__

print(simplejson.dumps(User('alice', 'alice@mail.com')))

if using standard json, you need to define a default function

import json
def default(o):
    return o._asdict()

print(json.dumps(User('alice', 'alice@mail.com'), default=default))

I simplifed this by removing the _asdict function with a lambda json.dumps(User('alice', 'alice@mail.com'), default=lambda x: x.__dict__)
M
Martlark

json is limited in terms of objects it can print, and jsonpickle (you may need a pip install jsonpickle) is limited in terms it can't indent text. If you would like to inspect the contents of an object whose class you can't change, I still couldn't find a straighter way than:

 import json
 import jsonpickle
 ...
 print  json.dumps(json.loads(jsonpickle.encode(object)), indent=2)

Note: that still they can't print the object methods.


D
Dan Brough

Here is my 3 cents ...
This demonstrates explicit json serialization for a tree-like python object.
Note: If you actually wanted some code like this you could use the twisted FilePath class.

import json, sys, os

class File:
    def __init__(self, path):
        self.path = path

    def isdir(self):
        return os.path.isdir(self.path)

    def isfile(self):
        return os.path.isfile(self.path)

    def children(self):        
        return [File(os.path.join(self.path, f)) 
                for f in os.listdir(self.path)]

    def getsize(self):        
        return os.path.getsize(self.path)

    def getModificationTime(self):
        return os.path.getmtime(self.path)

def _default(o):
    d = {}
    d['path'] = o.path
    d['isFile'] = o.isfile()
    d['isDir'] = o.isdir()
    d['mtime'] = int(o.getModificationTime())
    d['size'] = o.getsize() if o.isfile() else 0
    if o.isdir(): d['children'] = o.children()
    return d

folder = os.path.abspath('.')
json.dump(File(folder), sys.stdout, default=_default)

L
Lost Koder

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.


I liked this 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) ```
r
rectangletangle
import json

class Foo(object):
    def __init__(self):
        self.bar = 'baz'
        self._qux = 'flub'

    def somemethod(self):
        pass

def default(instance):
    return {k: v
            for k, v in vars(instance).items()
            if not str(k).startswith('_')}

json_foo = json.dumps(Foo(), default=default)
assert '{"bar": "baz"}' == json_foo

print(json_foo)

From doc: The parameter default(obj) is a function that should return a serializable version of obj or raise TypeError. The default default simply raises TypeError.
M
Martin Thoma

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):

to_json: 266,595 on 2018-06-27

toJSON: 96,307 on 2018-06-27

__json__: 8,504 on 2018-06-27

for_json: 6,937 on 2018-06-27

Deserialization (JSON -> Python):

from_json: 226,101 on 2018-06-27


j
jmhostalet

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))

M
Mark

If you don't mind installing a package for it, you can use json-tricks:

pip install json-tricks

After that you just need to import dump(s) from json_tricks instead of json, and it'll usually work:

from json_tricks import dumps
json_str = dumps(cls_instance, indent=4)

which'll give

{
        "__instance_type__": [
                "module_name.test_class",
                "MyTestCls"
        ],
        "attributes": {
                "attr": "val",
                "dct_attr": {
                        "hello": 42
                }
        }
}

And that's basically it!

This will work great in general. There are some exceptions, e.g. if special things happen in __new__, or more metaclass magic is going on.

Obviously loading also works (otherwise what's the point):

from json_tricks import loads
json_str = loads(json_str)

This does assume that module_name.test_class.MyTestCls can be imported and hasn't changed in non-compatible ways. You'll get back an instance, not some dictionary or something, and it should be an identical copy to the one you dumped.

If you want to customize how something gets (de)serialized, you can add special methods to your class, like so:

class CustomEncodeCls:
        def __init__(self):
                self.relevant = 42
                self.irrelevant = 37

        def __json_encode__(self):
                # should return primitive, serializable types like dict, list, int, string, float...
                return {'relevant': self.relevant}

        def __json_decode__(self, **attrs):
                # should initialize all properties; note that __init__ is not called implicitly
                self.relevant = attrs['relevant']
                self.irrelevant = 12

which serializes only part of the attributes parameters, as an example.

And as a free bonus, you get (de)serialization of numpy arrays, date & times, ordered maps, as well as the ability to include comments in json.

Disclaimer: I created json_tricks, because I had the same problem as you.


I've just tested json_tricks and it worked beautify (in 2019).
W
Wolfgang Fahl

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/


m
matthewlent

jsonweb seems to be the best solution for me. See http://www.jsonweb.info/en/latest/

from jsonweb.encode import to_object, dumper

@to_object()
class DataModel(object):
  def __init__(self, id, value):
   self.id = id
   self.value = value

>>> data = DataModel(5, "foo")
>>> dumper(data)
'{"__type__": "DataModel", "id": 5, "value": "foo"}'

Does it work well for nested objects? Including decoding and encoding
S
Sheikh Abdul Wahid
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))

m
mheyman

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

Works for any iterable instead of just list and tuple (it works for NumPy arrays, etc.) Works for dynamic types (ones that contain a __dict__). Includes native types float and None so they don't get converted to string. Classes that have __dict__ and members will mostly work (if the __dict__ and member names collide, you will only get one - likely the member) Classes that are lists and have members will look like a tuple of the list and a dictionary Python3 (that isinstance() call may be the only thing that needs changing)


W
Will Charlton

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)

s
sivabudh

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:

It's applicable to other ORMs (Django, etc) in general 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!


A
Adi Degani

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

Q
Quinten Cabo

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__()

D
Daniel Flippance

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.


N
NicoHood

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.


possibly because you'd need to define a __json__ property for each class, which can be sometimes a pain. also, dataclasses provides asdict so technically you don't need a __json__ property at all.
Sure, but what if you want to represent the json in a different way? Like in this case I combine first and last name. Thje asdict would not work for nested elements, right?
hmm, in that case I would suggest making first and last as InitVar (init-only) fields, and setting name field in the __post_init__ constructor. I think that should hopefully work to represent json in a diff format in this case. Also, i might be wrong but I believe asdict works for nested dataclasses as well.
But that does not work if you change the variables later on.
Hmm, to best of my understanding it should. can you provide an example of what you mean?
D
Dewsworld

I came up with my own solution. Use this method, pass any document (dict,list, ObjectId etc) to serialize.

def getSerializable(doc):
    # check if it's a list
    if isinstance(doc, list):
        for i, val in enumerate(doc):
            doc[i] = getSerializable(doc[i])
        return doc

    # check if it's a dict
    if isinstance(doc, dict):
        for key in doc.keys():
            doc[key] = getSerializable(doc[key])
        return doc

    # Process ObjectId
    if isinstance(doc, ObjectId):
        doc = str(doc)
        return doc

    # Use any other custom serializting stuff here...

    # For the rest of stuff
    return doc