将类实例序列化为JSON

259

我试图创建一个类实例的JSON字符串表示,但是遇到了困难。假设这个类是像这样构建的:

class testclass:
    value1 = "a"
    value2 = "b"

一个调用json.dumps的例子如下:
t = testclass()
json.dumps(t)

它失败了,并告诉我测试类不可JSON序列化。

TypeError: <__main__.testclass object at 0x000000000227A400> is not JSON serializable

我也尝试使用pickle模块:

t = testclass()
print(pickle.dumps(t, pickle.HIGHEST_PROTOCOL))

它提供类实例信息,但不提供类实例的序列化内容。
b'\x80\x03c__main__\ntestclass\nq\x00)\x81q\x01}q\x02b.'

我做错了什么?


https://dev59.com/rnE95IYBdhLWcg3wXcrd - CodeClown42
50
使用一行代码s = json.dumps(obj, default=lambda x: x.__dict__)将对象的实例变量(self.value1self.value2,...)进行序列化。这是最简单和最直接的方法,它可以序列化嵌套的对象结构。当任何给定对象不是直接可序列化时,将调用default函数。你也可以看看我下面的答案。我发现流行的答案过于复杂,可能在很长时间以前是正确的。 - codeman48
2
你的 testclass 没有 __init__() 方法,因此所有实例将共享在类语句中定义的两个类属性 (value1value2)。你明白类和实例之间的区别吗? - martineau
2
有一个Python库可以实现这个功能,它的网址是https://github.com/jsonpickle/jsonpickle。 - best wishes
17个回答

307
基本问题是JSON编码器 json.dumps() 默认只知道如何序列化内置类型的有限集合。这些类型在此处列出:https://docs.python.org/3.3/library/json.html#encoders-and-decoders 一个好的解决方案是让你的类继承自 JSONEncoder,然后实现 JSONEncoder.default() 函数,并使该函数发出你的类的正确 JSON。
一个简单的解决方案是在该实例的 .__dict__ 成员上调用 json.dumps()。它是一个标准的 Python 字典,如果你的类很简单,它将可以被 JSON 序列化。
class Foo(object):
    def __init__(self):
        self.x = 1
        self.y = 2

foo = Foo()
s = json.dumps(foo) # raises TypeError with "is not JSON serializable"

s = json.dumps(foo.__dict__) # s set to: {"x":1, "y":2}

以上的方法在这篇博客文章中有所讨论:

    使用 _dict_ 将任意Python对象序列化为JSON

当然,Python提供了一个内置函数可以访问 .__dict__,名为 vars()

因此,以上示例也可以这样完成:

s = json.dumps(vars(foo)) # s set to: {"x":1, "y":2}

4
我试过了。调用json.dumps(t.dict)的最终结果只是{}。 - ferhan
9
这是因为你的类没有 .__init__() 方法函数,所以你的类实例具有空字典。换句话说,{} 是你示例代码的正确结果。 - steveha
4
谢谢。这个方法很有效。我添加了一个没有参数的简单__init__函数,现在调用json.dumps(t.dict)可以返回正确格式的数据: {"value2": "345", "value1": "123"}之前我看过类似的帖子,但不确定是否需要为成员创建自定义序列化器,是否需要 init 也没有明确提到或者是我没注意到。感谢你。 - ferhan
4
这适用于单个类,但不适用于相关类对象。 - Nwawel A Iroume
4
没错。如果你有一个包含多个对象的列表对象,那么出现错误信息仍然是“is not JSON serializable”(无法将其序列化为JSON格式)。 - gies0r
显示剩余4条评论

88

我有一个非常适合我的方法,你可以试试:

json.dumps() 可以使用一个可选参数 default,你可以在这里指定一个自定义的序列化函数来处理未知类型,我的情况看起来像是这样:

def serialize(obj):
    """JSON serializer for objects not serializable by default json code"""

    if isinstance(obj, date):
        serial = obj.isoformat()
        return serial

    if isinstance(obj, time):
        serial = obj.isoformat()
        return serial

    return obj.__dict__

前两个if语句用于序列化日期和时间,然后返回obj.__dict__用于任何其他对象。

最终调用看起来像:

json.dumps(myObj, default=serialize)

当你需要序列化一个集合,并且不想为每个对象显式调用__dict__时,这将特别有用。它会自动为您完成。

到目前为止,对我来说运行得非常好,期待听取您的想法。


1
我收到了 NameError: name 'serialize' is not defined 的错误。有什么提示吗? - Kyle Delaney
非常好。仅适用于具有插槽的类:try: dict = obj.__dict__ except AttributeError: dict = {s: getattr(obj, s) for s in obj.__slots__ if hasattr(obj, s)} return dict - Fantastory
@Fantastory 为什么要使用 try-except 而不是直接使用 hasattr(obj, "__dict__") 进行检查?在我看来,后者更加简洁和专业。 - Mark Moretto
不试试Python的方式吗? - Fantastory

80
您可以在json.dumps()函数中指定default命名参数:
json.dumps(obj, default=lambda x: x.__dict__)

解释:

根据文档(2.73.6):

``default(obj)`` is a function that should return a serializable version
of obj or raise TypeError. The default simply raises TypeError.

(适用于Python 2.7和Python 3.x)

注意:在这种情况下,您需要使用实例变量,而不是变量,因为问题中的示例尝试这样做。(我假设问题提出者指的是类实例是一个类的对象)

我从@phihag的回答这里首先学到了它。发现它是完成工作最简单、最清洁的方法。


11
这个对我有用,但由于datetime.date成员的原因,我稍微修改了一下:default=lambda x: getattr(x, '__dict__', str(x)) - Dakota Hawkins
@Dakota 好的解决方法;datetime.date是C实现,因此它没有__dict__属性。我认为出于统一性的考虑,datetime.date应该拥有它... - codeman48

31

使用jsonpickle

import jsonpickle

object = YourClass()
json_object = jsonpickle.encode(object)

jsonpickle在我遇到Metaclasses#105之前一直运行良好。Enum(s)经历了奇怪的序列化过程。 - Nithish

25

我只是这样做:

data=json.dumps(myobject.__dict__)

这并不是完整的答案,如果你有某种复杂的对象类,肯定不会得到所有东西。但是我用它来处理一些简单对象。

一个它非常适用的类是你从OptionParser模块中获得的“options”类。 下面附上它及JSON请求本身。

  def executeJson(self, url, options):
        data=json.dumps(options.__dict__)
        if options.verbose:
            print data
        headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
        return requests.post(url, data, headers=headers)

如果你不在类中使用self,那么你可能想要移除它。 - SpiRail
4
只要该对象不由其他对象组成,那么这将有效运作。 - Haroldo_OK

8

Python3.x

我的知识水平下,我能达到的最佳方法就是这个。
请注意,此代码也处理set()。
这个方法是通用的,只需要类的扩展(第二个示例中)即可。
请注意,我只是在对文件进行操作,但很容易根据您的需求修改行为。

然而,这是一个CoDec。

通过更多的工作,你可以以其他方式构建你的类。 我假设默认构造函数来实例化它,然后更新类字典。

import json
import collections


class JsonClassSerializable(json.JSONEncoder):

    REGISTERED_CLASS = {}

    def register(ctype):
        JsonClassSerializable.REGISTERED_CLASS[ctype.__name__] = ctype

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        if isinstance(obj, JsonClassSerializable):
            jclass = {}
            jclass["name"] = type(obj).__name__
            jclass["dict"] = obj.__dict__
            return dict(_class_object=jclass)
        else:
            return json.JSONEncoder.default(self, obj)

    def json_to_class(self, dct):
        if '_set_object' in dct:
            return set(dct['_set_object'])
        elif '_class_object' in dct:
            cclass = dct['_class_object']
            cclass_name = cclass["name"]
            if cclass_name not in self.REGISTERED_CLASS:
                raise RuntimeError(
                    "Class {} not registered in JSON Parser"
                    .format(cclass["name"])
                )
            instance = self.REGISTERED_CLASS[cclass_name]()
            instance.__dict__ = cclass["dict"]
            return instance
        return dct

    def encode_(self, file):
        with open(file, 'w') as outfile:
            json.dump(
                self.__dict__, outfile,
                cls=JsonClassSerializable,
                indent=4,
                sort_keys=True
            )

    def decode_(self, file):
        try:
            with open(file, 'r') as infile:
                self.__dict__ = json.load(
                    infile,
                    object_hook=self.json_to_class
                )
        except FileNotFoundError:
            print("Persistence load failed "
                  "'{}' do not exists".format(file)
                  )


class C(JsonClassSerializable):

    def __init__(self):
        self.mill = "s"


JsonClassSerializable.register(C)


class B(JsonClassSerializable):

    def __init__(self):
        self.a = 1230
        self.c = C()


JsonClassSerializable.register(B)


class A(JsonClassSerializable):

    def __init__(self):
        self.a = 1
        self.b = {1, 2}
        self.c = B()

JsonClassSerializable.register(A)

A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
print(b.b)
print(b.c.a)

编辑

通过进一步的研究,我找到了一种使用元类来实现泛化而无需调用SUPERCLASS注册方法的方法。

import json
import collections

REGISTERED_CLASS = {}

class MetaSerializable(type):

    def __call__(cls, *args, **kwargs):
        if cls.__name__ not in REGISTERED_CLASS:
            REGISTERED_CLASS[cls.__name__] = cls
        return super(MetaSerializable, cls).__call__(*args, **kwargs)


class JsonClassSerializable(json.JSONEncoder, metaclass=MetaSerializable):

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        if isinstance(obj, JsonClassSerializable):
            jclass = {}
            jclass["name"] = type(obj).__name__
            jclass["dict"] = obj.__dict__
            return dict(_class_object=jclass)
        else:
            return json.JSONEncoder.default(self, obj)

    def json_to_class(self, dct):
        if '_set_object' in dct:
            return set(dct['_set_object'])
        elif '_class_object' in dct:
            cclass = dct['_class_object']
            cclass_name = cclass["name"]
            if cclass_name not in REGISTERED_CLASS:
                raise RuntimeError(
                    "Class {} not registered in JSON Parser"
                    .format(cclass["name"])
                )
            instance = REGISTERED_CLASS[cclass_name]()
            instance.__dict__ = cclass["dict"]
            return instance
        return dct

    def encode_(self, file):
        with open(file, 'w') as outfile:
            json.dump(
                self.__dict__, outfile,
                cls=JsonClassSerializable,
                indent=4,
                sort_keys=True
            )

    def decode_(self, file):
        try:
            with open(file, 'r') as infile:
                self.__dict__ = json.load(
                    infile,
                    object_hook=self.json_to_class
                )
        except FileNotFoundError:
            print("Persistence load failed "
                  "'{}' do not exists".format(file)
                  )


class C(JsonClassSerializable):

    def __init__(self):
        self.mill = "s"


class B(JsonClassSerializable):

    def __init__(self):
        self.a = 1230
        self.c = C()


class A(JsonClassSerializable):

    def __init__(self):
        self.a = 1
        self.b = {1, 2}
        self.c = B()


A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
# 1
print(b.b)
# {1, 2}
print(b.c.a)
# 1230
print(b.c.c.mill)
# s

我喜欢它。这是一个惊人的实现。如果可以的话,我只有一个问题,为什么在元类中使用_call_方法而不是_new_方法?我问这个问题是因为每次创建新实例时都会调用_call_方法,所以代码尝试为每个新实例注册类。这不是什么大问题,正如我写的那样,我喜欢这个实现,它看起来真的很棒 :-) - SagiZiv

8
JSON并不适合序列化Python中的任意对象,它非常适合序列化字典对象,但通常情况下应该使用pickle模块。pickle输出的内容并不容易阅读,但反序列化时应该没有问题。如果您坚持使用JSON,可以尝试使用jsonpickle模块,这是一种有趣的混合方法。
链接:https://github.com/jsonpickle/jsonpickle

11
我认为Pickle的主要问题在于它是Python特定的格式,而JSON是一个平台无关的格式。如果你正在编写web应用程序或某些移动应用程序的后端,则JSON特别有用。话虽如此,感谢您指出jsonpickle。 - Haroldo_OK
@Haroldo_OK,jsonpickle仍然可以导出JSON,只是不太容易阅读。 - Caelum
这个问题不是关于序列化,而是关于将其序列化为JSON格式。有很多原因需要这样做,而pickle对于大多数情况来说都是一个糟糕的工具。 - Tarynn

6

这可以很容易地使用pydantic来处理,因为它已经内置了此功能。

选项1:普通方式

from pydantic import BaseModel

class testclass(BaseModel):
    value1: str = "a"
    value2: str = "b"

test = testclass()

>>> print(test.json(indent=4))
{
    "value1": "a",
    "value2": "b"
}

选项二:使用pydantic的数据类。
import json
from pydantic.dataclasses import dataclass
from pydantic.json import pydantic_encoder

@dataclass
class testclass:
    value1: str = "a"
    value2: str = "b"

test = testclass()
>>> print(json.dumps(test, indent=4, default=pydantic_encoder))
{
    "value1": "a",
    "value2": "b"
}

5
这里有两个简单的函数,可以对任何非复杂类进行序列化,与之前解释的一样,没有花哨的东西。
我将其用于配置类型的内容,因为我可以向类中添加新成员而不需要进行代码调整。
import json

class SimpleClass:
    def __init__(self, a=None, b=None, c=None):
        self.a = a
        self.b = b
        self.c = c

def serialize_json(instance=None, path=None):
    dt = {}
    dt.update(vars(instance))

    with open(path, "w") as file:
        json.dump(dt, file)

def deserialize_json(cls=None, path=None):
    def read_json(_path):
        with open(_path, "r") as file:
            return json.load(file)

    data = read_json(path)

    instance = object.__new__(cls)

    for key, value in data.items():
        setattr(instance, key, value)

    return instance

# Usage: Create class and serialize under Windows file system.
write_settings = SimpleClass(a=1, b=2, c=3)
serialize_json(write_settings, r"c:\temp\test.json")

# Read back and rehydrate.
read_settings = deserialize_json(SimpleClass, r"c:\temp\test.json")

# results are the same.
print(vars(write_settings))
print(vars(read_settings))

# output:
# {'c': 3, 'b': 2, 'a': 1}
# {'c': 3, 'b': 2, 'a': 1}

嘿@GBGOLC, 感谢你提供的这段棒极了的代码!由于我在使用该代码的地方没有文件,因此我尝试使用json.loads和dumps来实现它。我正在努力解码我的示例中引用了车轮的Vehicle类。 这是我所做的更改。 非常感谢您的帮助。 - Tom
1
好的,我解决了自己的问题:由于decode_改变了它所传递的实例,因此没有返回值可以传递给新实例。你的代码很好,只需要改变我的调用方式就可以了。再次感谢你提供的优秀代码! - Tom

5

我认为与接受的答案中建议的继承相比,使用多态性更好。否则,您必须拥有一个大的if else语句来自定义每个对象的编码。这意味着创建一个通用的默认JSON编码器,如下所示:

def jsonDefEncoder(obj):
   if hasattr(obj, 'jsonEnc'):
      return obj.jsonEnc()
   else: #some default behavior
      return obj.__dict__

然后在你想要序列化的每个类中都需要有一个jsonEnc()函数。例如:

class A(object):
   def __init__(self,lengthInFeet):
      self.lengthInFeet=lengthInFeet
   def jsonEnc(self):
      return {'lengthInMeters': lengthInFeet * 0.3 } # each foot is 0.3 meter

然后您调用 json.dumps(classInstance,default = jsonDefEncoder)


这是正确的答案。该类知道如何将自身序列化。很遗憾,您不能直接将方法名称传递给json.dumps。 - Tarynn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接