如何使一个类能够被JSON序列化

1366

如何使一个Python类可序列化?

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

尝试将数据序列化为JSON格式:

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

270
很不幸,所有的答案似乎都回答了“我如何序列化一个类?”这个问题,而不是行动问题“我如何使一个类可序列化?”这些答案假设你自己进行序列化,而不是将对象传递给某个其他模块进行序列化。 - Kyle Delaney
7
如果您正在使用Python3.5+版本,您可以使用jsons库。它将(递归地)将您的对象和其所有属性转换为字典。import jsons 查看下面的答案 - 它可以正常工作。 - tswaehn
13
@KyleDelaney 我真的希望有一个接口/魔法方法可以实现我也能被序列化。我想我必须要实现一个.to_dict()函数或者其他类似的东西,在对象传递给尝试序列化它的模块之前调用它。 - Felix B.
73
在11年里,令人惊讶的是没有一个回答能够解决这个问题。OP 表示他想使用 json.dumps,但是所有的回答,包括获得奖励的回答,都涉及创建一个自定义编码器,这完全回避了问题的核心。 - Mike
1
话虽如此,这个问题现在作为一个规范存在,因此吸引那些让初学者知道正确做法的答案是完全合理的。 - Karl Knechtel
显示剩余8条评论
44个回答

844

这里有一个简单的解决方案来实现一个简单的功能:

.toJSON()方法

不需要一个可序列化为JSON的类,而是实现一个序列化方法:

import json

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

那么你只需要调用它进行序列化:

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

print(me.toJSON())

将输出:

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

179
非常有限。如果您有一个字典 {"foo":"bar","baz":"bat"},那么它将很容易地序列化为JSON。但是,如果您有 {"foo":"bar","baz":MyObject()},那么就不行了。理想的情况是,嵌套对象应该递归地被序列化为JSON,而不是显式地被序列化。 - Mark E. Haase
43
它仍然可以工作。你漏掉了 o.__dict___。试试你自己的例子:class MyObject(): def __init__(self): self.prop = 1 j = json.dumps({ "foo": "bar", "baz": MyObject() }, default=lambda o: o.__dict__) - Onur Yıldırım
20
这个解决方案是否可逆?即,是否可以从JSON重新构建对象? - Jorge Leitao
17
这不适用于 datetime.datetime 实例。它会抛出以下错误:'datetime.datetime' object has no attribute '__dict__' - Bruno Finger
22
我可能漏看了什么,但这似乎不起作用(即json.dumps(me)没有调用ObjecttoJSON方法)。 - cglacet
显示剩余13条评论

715

你有对期望输出的想法吗?比如说,这样行吗?

>>> f  = FileItem("/foo/bar")
>>> magic(f)
'{"fname": "/foo/bar"}'
在这种情况下,你可以简单地调用 json.dumps(f.__dict__)
如果你想要更自定义的输出,那么你需要继承 JSONEncoder 并实现自己的自定义序列化。
以下是一个简单的示例。
>>> from json import JSONEncoder
>>> class MyEncoder(JSONEncoder):
        def default(self, o):
            return o.__dict__    

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

然后你将这个类作为cls关键字参数传递给json.dumps()方法:

json.dumps(cls=MyEncoder)

如果您还想进行解码,则需要向 JSONDecoder 类提供一个自定义的 object_hook。例如:

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

65
使用__dict__并不适用于所有情况。如果对象实例化后未设置属性,则__dict__可能不会完全填充。在上面的示例中,您没有问题,但是如果您有类属性也想进行编码,则这些属性将不会在__dict__中列出,除非它们在类的__init__调用或在对象实例化后通过某种其他方式进行了修改。 - Kris Hardy
10
+1,但是作为对象钩子使用的from_json()函数应该加上else: return json_object语句,以便处理普通对象。 - jogojapan
11
如果你在新式类中使用了__slots__,那么__dict__也无法使用。 - badp
12
您可以使用自定义的 JSONEncoder,如上所述,创建一个自定义协议,例如检查是否存在 __json_serializable__ 方法并调用它来获取对象的 JSON 可序列化表示。这与其他 Python 模式保持一致,例如 __getitem____str____eq____len__ - jpmc26
6
__dict__ 也不能递归地工作,例如,如果您对象的属性是另一个对象。 - Neel
显示剩余5条评论

256
对于更复杂的类,您可以考虑使用工具jsonpickle
jsonpickle是一个用于将复杂的Python对象序列化和反序列化为JSON的Python库。
标准的Python库,如stdlib的json、simplejson和demjson,只能处理具有直接JSON等效的Python原语(例如字典、列表、字符串、整数等)。jsonpickle在这些库的基础上构建,并允许将更复杂的数据结构序列化为JSON。jsonpickle具有高度可配置和可扩展的特性,允许用户选择JSON后端并添加其他后端。
将对象转换为JSON字符串:
import jsonpickle
json_string = jsonpickle.encode(obj)

从JSON字符串中重新创建一个Python对象:
recreated_obj = jsonpickle.decode(json_string)

(链接到PyPi上的jsonpickle)


75
从C#转过来,这就是我期望的。一个简单的一行代码,不需要与类打交道。 - Jerther
4
jsonpickle非常棒。它完美地处理了一个庞大、复杂、混乱的对象,其中包含多层类。 - wisbucky
有没有一个正确保存到文件的示例?文档只展示了如何编码和解码 jsonpickle 对象。此外,它无法解码包含 Pandas 数据帧的字典字典。 - user5359531
7
你可以使用obj = jsonpickle.decode(file.read())file.write(jsonpickle.encode(obj))。这些代码可用于将JSON格式的数据解码为Python对象,并将Python对象编码为JSON格式并写入文件中。 - Kilian Batzner
我在使用jsonpickle解码数据帧时遇到了失败的情况,导致jupyter内核崩溃。由于数据帧位于一个类中,因此我不得不使用这个解决方法来序列化该类。具体方法请参考此链接:https://github.com/jsonpickle/jsonpickle/issues/213 - undefined
显示剩余7条评论

206

大多数答案都涉及更改对 json.dumps() 的调用,但这并不总是可能或理想的(例如可能发生在框架组件内)。

如果您希望能够像原样调用 json.dumps(obj),那么一个简单的解决方案是继承 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

如果你的类只是基本数据表示,那么这将起作用,对于更棘手的事情,你总是可以在调用dict.__init__()时显式设置键。

这能够起作用是因为json.dumps()通过一个相当不符合Python风格的isinstance(value, dict)检查对象是否属于几个已知类型之一 - 因此,如果你真的不想从dict继承,使用__class__和其他一些方法也是可能的。


9
这确实可以成为一个很好的解决方案 :) 我相信对于我的情况是这样的。优点:通过将对象变成带有 init 的类来传达对象的“形状”,它本质上是可序列化的,而且看起来也容易解释,可以作为 repr 表示。 - PascalVKooten
4
“点访问”仍然缺失 :( - PascalVKooten
3
啊,看起来好像可以工作了!谢谢,不确定为什么这不是被接受的答案。我完全同意更改“dumps”不是一个好的解决方案。顺便说一下,在大多数情况下,您可能希望将“dict”继承与委托结合使用,这意味着您将在类内部拥有一些“dict”类型属性,然后将此属性作为参数传递给初始化,例如super().__init__(self.elements) - cglacet
3
这个解决方案有点hacky - 对于真正的、生产质量的解决方案,用jsonpickle.encode()和jsonpickle.decode()替换json.dumps()和json.loads()。你将避免编写丑陋的样板代码,最重要的是,如果你能够pickle对象,那么你应该能够在没有样板代码的情况下序列化它(复杂的容器/对象将正常工作)。 - kfmfe04
6
这个回答解决的情况是,当你无法控制调用json.dumps的代码时。 - andyhasit
显示剩余13条评论

185

正如其他答案中提到的那样,您可以将一个函数传递给json.dumps来将默认不支持的对象转换为支持的类型。令人惊讶的是,它们中没有任何一种提到最简单的情况,即使用内置函数vars将对象转换为包含其所有属性的字典:

json.dumps(obj, default=vars)

请注意,这仅涵盖基本情况。如果您需要针对某些类型进行更具体的序列化(例如,排除某些属性或对象没有__dict__属性),则需要使用自定义函数或像其他答案中描述的JSONEncoder


1
“default=vars”这个表述不太清楚,它是否意味着“vars”是默认的序列化器?如果不是的话:这并不能真正解决你无法影响“json.dumps”如何调用的情况。如果你只是将一个对象传递给库,而该库在该对象上调用“json.dumps”,那么即使你已经实现了“vars”,也没有什么帮助,因为该库不使用这种方式来调用“dumps”。从这个意义上说,它等同于自定义“JSONEncoder”。 - Felix B.
14
对于某些对象,此方法会抛出“vars()参数必须具有__dict__属性”的错误提示。 - JustAMartin
7
这可能是最佳解决方案,最不具侵入性且最易于理解的。 - Leonmax
3
谢谢,使用具有适当定义的库非常简单。 - PKiong
1
有没有解决错误 vars() argument must have __dict__ attribute 的方法? - undefined
显示剩余2条评论

99

只需要像这样在你的类中添加to_json方法:

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

添加此代码(来自这个答案),放在所有内容的顶部某个位置:

from json import JSONEncoder

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

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

这将在导入json模块时进行猴子补丁,因此JSONEncoder.default()会自动检查特殊的to_json()方法,并在找到时使用它来编码对象。
正如Onur所说的那样,但这次您不必更新项目中的每个json.dumps()

14
非常感谢!这是唯一一个让我能够做到我想要的事情的答案:能够在不改变现有代码的情况下序列化对象。其他方法大多数都对我不起作用。该对象是在第三方库中定义的,并且序列化代码也是第三方的。更改它们会很麻烦。使用您的方法,我只需要执行TheObject.to_json = my_serializer即可。 - Yongwei Wu
2
这是正确的答案。 我做了一个小变化: _fallback = json._default_encoder.default json._default_encoder.default = lambda obj: getattr(obj.__class__, "to_json", _fallback)(obj) - Kjir
肯定有比在所有东西的顶部进行JSON编码器的黑客攻击更好的方法。这种方法太脆弱了,不能成为可靠的解决方案。 - undefined

71

简而言之:复制粘贴以下选项1或选项2

完整回答:使Python的json模块与您的类一起使用

也就是解决:json.dumps({ "thing": YOUR_CLASS() })


解释:

  • 是的,存在可靠的解决方案。
  • 不,没有Python的“官方”解决方案。
    • 所谓官方解决方案,是指截至2023年,无法像JavaScript中的toJSON那样向类添加方法,也无法将类注册到内置的json模块中。当执行json.dumps([1,2, your_obj])这样的代码时,Python不会检查查找表或对象方法。
    • 我不确定为什么其他答案没有解释这一点。
    • 最接近的官方方法可能是andyhasit的答案,即从字典继承。然而,从字典继承对于许多自定义类(如AdvancedDateTime或pytorch张量)效果不佳。
  • 理想的解决方法如下:
    • 在您的类中添加def __json__(self)方法。
    • 修改json.dumps以检查__json__方法(会影响到任何地方,甚至是导入json的pip模块)。
    • 注意:通常修改内置的东西并不是一个好主意,但是这个改变应该没有副作用,即使它被不同的代码库多次应用。它在运行时完全可逆(如果一个模块想要撤消修改)。无论好坏,这是目前可以做到的最好的方法。


选项1:让一个模块来进行修补。

pip install json-fix
Fancy John's answer的扩展+打包版本,感谢@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"

就是这样。


使用示例:

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

为了使`json.dumps`适用于Numpy数组、Pandas DataFrames和其他第三方对象,请参阅该模块(仅需约2行代码,但需要解释)。


它是如何工作的?嗯...

选项2:自行修补json.dumps


注意:这种方法是简化的,它在已知的边缘情况下失败(例如:如果您的自定义类继承自dict或另一个内置类),并且它无法控制外部类(如numpy数组、datetime、dataframes、tensors等)的json行为。

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"

_

所有其他答案似乎都是关于“序列化自定义对象的最佳实践/方法”的内容。
这个问题已经在文档中这里有所涵盖(搜索“complex”以获取编码复数的示例)。

5
在整个代码库中修改json.dumps有点过于激进,但在我看来,这显然是最好的解决方案。 - rjh
好的解决方案。有没有json.loads的等价物? - Sam
2
很遗憾,@Sam和他们的种类基本上是不可能的;json-dumping实际上是一个单向操作。例如:想象一个BigInt类,它将自己转换为字符串以供json.dumps使用。现在考虑一下json文件中的某个随机字符串值。也许这个字符串值包含所有数字,那么它应该被加载为BigInt吗?那些只是巧合包含所有数字但应保持为字符串的字符串又怎么办?json.loads无法知道,所以你必须像BigInt.from_json(a_str)这样做,使用你知道应该是BigInt的字符串。 - Jeff Hykin

68

我喜欢Onur的回答,但我会扩展以包括一个可选的toJSON()方法,使对象能够序列化自己:

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

我发现这是在使用现有的 json.dumps 和引入自定义处理之间取得最佳平衡的方法。谢谢! - Daniel Buckmaster
20
我很喜欢这个代码片段,不过我会使用 if 'toJSON' in obj.__attrs__(): 替代 try-catch 来避免悄无声息的失败(如果由于某些原因而导致 toJSON() 失败,而不是因为函数本身不存在)。这种失败可能会导致数据损坏。 - thclark
9
据我理解,Python 习惯上采用“宁愿请求原谅也不要征得许可”的方式,因此使用 try-except 是正确的方法,但应该捕获正确的异常,对于这种情况应该捕获 AttributeError 异常。 - Phil
5
几年过去了,我变得更年长、更明智,我同意你的观点。 - thclark
4
这段话的意思是:这个错误应该显式地捕获AttributeError。我的翻译如下:This error should be caught explicitly as an AttributeError. - juanpa.arrivillaga
2
如果在 obj.toJSON() 内部引发了 AttributeError,那该怎么办? - artm

46
如果您使用的是Python3.5+,您可以使用jsons。(PyPi: https://pypi.org/project/jsons/)它将递归地将您的对象(及其所有属性)转换为字典。
import jsons

a_dict = jsons.dump(your_object)

或者如果您需要一个字符串:

a_str = jsons.dumps(your_object)

或者如果你的类实现了 jsons.JsonSerializable

a_dict = your_object.json

7
如果您能够使用Python 3.7+,我发现将Python类转换为字典和JSON字符串(反之亦然)的最简洁解决方案是将jsons库与dataclasses混合使用。目前为止,对我来说一切都很好! - Ruluk
25
这是一个外部库,不是Python标准安装程序内置的。 - Noumenon
你可以使用 __slots__,但并不是必须的。只有在根据特定类的签名进行转储时才需要使用 __slots__。在即将发布的版本1.1.0中,这也不再是必须的。 - R H
你是如何在可视化/图像中绘制JSON的呢?顺便说一下,这是怎么工作的。json.dumps(obj) 然后 json.loads(obj) - RustyShackleford
一个仍然缺失的功能是人类可读性选项。漂亮打印/缩进尚未实现(至少在v1.6.1之前还没有)。 - Sam
显示剩余2条评论

43

另一种选择是将 JSON 转储封装在自己的类中:

import json

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

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

或者,更好的方法是从JsonSerializable类继承FileItem类:

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

测试:

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

4
你好,我不太喜欢这种“自定义编码器”的方法,如果你能使你的类可JSON序列化会更好。我已经尝试了很多次,但都没有成功。你有什么想法吗?问题在于json模块将测试您的类与内置的Python类型进行比较,并且即使是针对自定义类,它也会建议制作自己的编码器。你能否伪造一下?这样我的类就可以像简单列表一样对待JSON模块了吗?我试过__subclasscheck__和__instancecheck__但没有效果。 - Bojan Radojevic
@ADRENALIN 如果所有类属性值都可序列化且您不介意使用hack,则可以从主类型(可能是dict)继承。您还可以使用jsonpickle或json_tricks或其他替代标准的编码器(仍然是自定义编码器,但无需编写或调用)。前者将实例进行pickle,后者将其存储为属性字典,您可以通过实现__json__encode__ / __json_decode__来更改它(揭示:我制作了最后一个)。 - Mark
4
这并不会使该对象对于 json 类来说是可序列化的。它只是提供了一个返回 json 字符串的方法(微不足道)。因此 json.dumps(f) 将失败。这不是被要求的。 - omni

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