Dataclasses JSON
该库提供了一个简单的API,用于将dataclasses编码和解码为JSON格式。
入门非常简单。
README / 文档网站。具有导航栏和搜索功能,应该与此README完全一致 -- 快来看看吧!
快速开始
pip install dataclasses-json
from dataclasses import dataclass
from dataclasses_json import dataclass_json
@dataclass_json
@dataclass
class Person:
name: str
person = Person(name='lidatong')
person.to_json() # '{"name": "lidatong"}' <- 这是一个字符串
person.to_dict() # {'name': 'lidatong'} <- 这是一个字典
Person.from_json('{"name": "lidatong"}') # Person(1)
Person.from_dict({'name': 'lidatong'}) # Person(1)
# 你还可以使用另一种API来应用_模式验证_
# 这对于"类型化"的Python代码很有用
Person.from_json('{"name": 42}') # 这是可以的。42不是`str`类型,但
# dataclass创建不验证类型
Person.schema().loads('{"name": 42}') # 错误!引发`ValidationError`
如果你想使用驼峰命名法的JSON怎么办?
# 与上面相同的导入,另外导入`LetterCase`
from dataclasses import dataclass
from dataclasses_json import dataclass_json, LetterCase
@dataclass_json(letter_case=LetterCase.CAMEL) # 现在所有字段都以驼峰命名法编码/解码
@dataclass
class ConfiguredSimpleExample:
int_field: int
ConfiguredSimpleExample(1).to_json() # {"intField": 1}
ConfiguredSimpleExample.from_json('{"intField": 1}') # ConfiguredSimpleExample(1)
支持的类型
它是递归的(见下面的注意事项),所以你可以轻松处理嵌套的数据类。 除了py to JSON 表格中支持的类型外,这个库还支持以下类型:
-
支持任意的Collection类型。 Mapping类型被编码为JSON对象,
str
类型被编码为JSON字符串。 其他所有Collection类型都被编码为JSON数组,但解码时会还原为原始的集合类型。 -
datetime对象。
datetime
对象使用timestamp被编码为float
(JSON数字)。 如datetime
文档中所述,如果你的datetime
对象是naive的,调用.timestamp()
时会假定使用你的系统本地时区。在你的数据类中对应于datetime
字段的JSON数字会被解码为datetime-aware对象,tzinfo
设置为你的系统本地时区。 因此,如果你编码一个datetime-naive对象,你将解码得到一个datetime-aware对象。这很重要,因为编码和解码不会严格地互为逆操作。如果你想覆盖这个默认行为(例如,如果你想使用ISO),请参阅这一节。 -
UUID对象。它们被编码为
str
(JSON字符串)。 -
Decimal对象。它们也被编码为
str
。
最新版本同时兼容Python 3.7和Python 3.6(使用dataclasses向后移植)。
使用方法
方法1:类装饰器
from dataclasses import dataclass
from dataclasses_json import dataclass_json
@dataclass_json
@dataclass
class Person:
name: str
lidatong = Person('lidatong')
# 编码为JSON
lidatong.to_json() # '{"name": "lidatong"}'
# 从JSON解码
Person.from_json('{"name": "lidatong"}') # Person(name='lidatong')
注意,@dataclass_json
装饰器必须堆叠在@dataclass
装饰器之上(顺序很重要!)
方法2:继承一个混入类
from dataclasses import dataclass
from dataclasses_json import DataClassJsonMixin
@dataclass
class Person(DataClassJsonMixin):
name: str
lidatong = Person('lidatong')
# 与上面的方法1不同的例子,但使用方法完全相同
assert Person.from_json(lidatong.to_json()) == lidatong
选择适合你口味的方法。注意,在使用_静态分析_工具(如代码检查、类型检查)时,混入方法有更好的支持,但在_运行时_使用中,这些实现上的差异是不可见的。
我如何...
将我的数据类与JSON数组或对象一起使用?
from dataclasses import dataclass
from dataclasses_json import dataclass_json
@dataclass_json
@dataclass
class Person:
name: str
将我的数据类实例编码为JSON数组
people_json = [Person('lidatong')]
Person.schema().dumps(people_json, many=True) # '[{"name": "lidatong"}]'
将包含我的数据类实例的JSON数组解码
people_json = '[{"name": "lidatong"}]'
Person.schema().loads(people_json, many=True) # [Person(name='lidatong')]
将我的数据类编码为更大的JSON对象的一部分(例如HTTP请求/响应)
import json
response_dict = {
'response': {
'person': Person('lidatong').to_dict()
}
}
response_json = json.dumps(response_dict)
在这种情况下,我们分两步进行。首先,我们使用.to_dict
将数据类编码为Python字典而不是JSON字符串。
其次,我们利用内置的json.dumps
将我们的dataclass
序列化为JSON字符串。
将包含我的数据类的更大JSON对象解码(例如HTTP响应)
import json
response_dict = json.loads('{"response": {"person": {"name": "lidatong"}}}')
person_dict = response_dict['response']
person = Person.from_dict(person_dict)
与上面的编码类似,我们利用内置的json
模块。
首先,调用json.loads
将整个JSON对象读入字典。然后我们访问包含我们想要解码的Person
编码字典的值的键(response_dict['response']
)。
其次,我们使用Person.from_dict
加载该字典。
编码或解码为Python列表/字典而不是JSON?
这可以通过调用.schema()
然后使用相应的编码器/解码器方法来实现,即.load(...)
/.dump(...)
。
编码为单个Python字典
person = Person('lidatong')
person.to_dict() # {'name': 'lidatong'}
编码为Python字典列表
people = [Person('lidatong')]
Person.schema().dump(people, many=True) # [{'name': 'lidatong'}]
将字典解码为单个数据类实例
person_dict = {'name': 'lidatong'}
Person.from_dict(person_dict) # Person(name='lidatong')
将字典列表解码为数据类实例列表
people_dicts = [{"name": "lidatong"}]
Person.schema().load(people_dicts, many=True) # [Person(name='lidatong')]
编码或解码为驼峰命名法(或kebab-case)?
按照惯例,JSON使用驼峰命名法,而Python成员按惯例使用蛇形命名法。
你可以在类级别和字段级别配置它以从其他命名方案编码/解码。
from dataclasses import dataclass, field
from dataclasses_json import LetterCase, config, dataclass_json
# 在类级别更改命名方案
@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Person:
given_name: str
family_name: str
Person('Alice', 'Liddell').to_json() # '{"givenName": "Alice"}'
Person.from_json('{"givenName": "Alice", "familyName": "Liddell"}') # Person('Alice', 'Liddell')
# 在字段级别
@dataclass_json
@dataclass
class Person:
given_name: str = field(metadata=config(letter_case=LetterCase.CAMEL))
family_name: str
Person('Alice', 'Liddell').to_json() # '{"givenName": "Alice"}'
# 注意`family_name`字段仍然是蛇形命名法,因为上面没有配置它
Person.from_json('{"givenName": "Alice", "family_name": "Liddell"}') # Person('Alice', 'Liddell')
本库假设你的字段遵循Python的蛇形命名法命名约定。
如果你的字段一开始就不是snake_case
,而你试图参数化LetterCase
,
那么编码/解码的行为是未定义的(很可能会导致微妙的错误)。
使用不同的名称进行编码或解码
from dataclasses import dataclass, field
from dataclasses_json import config, dataclass_json
@dataclass_json
@dataclass
class Person:
given_name: str = field(metadata=config(field_name="overriddenGivenName"))
Person(given_name="Alice") # Person('Alice')
Person.from_json('{"overriddenGivenName": "Alice"}') # Person('Alice')
Person('Alice').to_json() # {"overriddenGivenName": "Alice"}
在解码时处理缺失或可选的字段值?
默认情况下,如果你的数据类中使用了default
或default_factory
的任何字段,在解码时,如果JSON中缺少相应的字段,这些字段将使用提供的默认值填充。
解码缺少字段的JSON
@dataclass_json
@dataclass
class Student:
id: int
name: str = 'student'
Student.from_json('{"id": 1}') # Student(id=1, name='student')
注意from_json
在JSON中缺少name
字段时,用指定的默认值'student'填充了该字段。
有时你可能有一些类型为Optional
的字段,但你不一定想为它们指定默认值。在这种情况下,你可以使用infer_missing
关键字参数,使from_json
将缺失的字段值推断为None
。
解码没有默认值的可选字段
@dataclass_json
@dataclass
class Tutor:
id: int
student: Optional[Student] = None
Tutor.from_json('{"id": 1}') # Tutor(id=1, student=None)
个人建议你利用数据类的默认值而不是使用infer_missing
,但如果出于某种原因你需要将JSON解码行为与字段的默认值解耦,这种方法可以让你做到这一点。
如何处理JSON中未知/额外的字段?
默认情况下,当json_dataclass
接收到未定义的输入参数时,具体行为取决于实现方式。
(from_dict
方法会忽略它们,而使用schema()
加载时会引发ValidationError。)
有三种方法可以自定义这种行为。
假设你想用以下字典实例化一个数据类:
dump_dict = {"endpoint": "some_api_endpoint", "data": {"foo": 1, "bar": "2"}, "undefined_field_name": [1, 2, 3]}
- 你可以通过将
undefined
关键字设置为Undefined.RAISE
来强制始终引发错误 (不区分大小写的字符串'RAISE'也可以)。当然,如果你不传入任何未定义的参数,它会正常工作。
from dataclasses_json import Undefined
@dataclass_json(undefined=Undefined.RAISE)
@dataclass()
class ExactAPIDump:
endpoint: str
data: Dict[str, Any]
dump = ExactAPIDump.from_dict(dump_dict) # 引发UndefinedParameterError
- 你可以通过将
undefined
关键字设置为Undefined.EXCLUDE
来简单地忽略任何未定义的参数 (不区分大小写的字符串'EXCLUDE'也可以)。注意,你将无法使用to_dict
检索它们:
from dataclasses_json import Undefined
@dataclass_json(undefined=Undefined.EXCLUDE)
@dataclass()
class DontCareAPIDump:
endpoint: str
data: Dict[str, Any]
dump = DontCareAPIDump.from_dict(dump_dict) # DontCareAPIDump(endpoint='some_api_endpoint', data={'foo': 1, 'bar': '2'})
dump.to_dict() # {"endpoint": "some_api_endpoint", "data": {"foo": 1, "bar": "2"}}
- 你可以将它们保存在一个通用字段中,以便稍后进行处理。只需将
undefined
关键字设置为Undefined.INCLUDE
(不区分大小写的字符串'INCLUDE'也可以),并定义一个类型为CatchAll
的字段,所有未知值都将存储在其中。 这只是一个可以容纳任何内容的字典。 如果没有未定义的参数,这将是一个空字典。
from dataclasses_json import Undefined, CatchAll
@dataclass_json(undefined=Undefined.INCLUDE)
@dataclass()
class UnknownAPIDump:
endpoint: str
data: Dict[str, Any]
unknown_things: CatchAll
dump = UnknownAPIDump.from_dict(dump_dict) # UnknownAPIDump(endpoint='some_api_endpoint', data={'foo': 1, 'bar': '2'}, unknown_things={'undefined_field_name': [1, 2, 3]})
dump.to_dict() # {'endpoint': 'some_api_endpoint', 'data': {'foo': 1, 'bar': '2'}, 'undefined_field_name': [1, 2, 3]}
注意:
- 使用
Undefined.INCLUDE
时,如果你没有指定恰好一个CatchAll
类型的字段,将会引发UndefinedParameterError
。 - 请注意,
LetterCase
不会影响写入CatchAll
字段的值,它们将保持原样。 - 当为
CatchAll
字段指定默认值(或默认工厂)时,例如unknown_things: CatchAll = None
,如果没有未定义的参数,将使用默认值而不是空字典。 - 使用非关键字参数调用__init__会将参数解析为已定义的字段,并将其他所有内容写入通用字段。
-
这3个选项也适用于使用
schema().loads
和schema().dumps
,只要你不通过指定schema(unknown=<marshmallow值>)
来覆盖它。 marshmallow使用相同的3个关键字['include', 'exclude', 'raise']。 -
这3个操作也适用于使用
__init__
,例如UnknownAPIDump(**dump_dict)
将不会引发TypeError
,而是将所有未知值写入标记为CatchAll
的字段。 标记为EXCLUDE
的类也会简单地忽略未知参数。请注意,标记为RAISE
的类如果提供了未知关键字,仍会引发TypeError
,而不是UndefinedParameterError
。
如何覆盖特定字段的默认编码/解码/marshmallow字段?
参见覆盖
如何处理递归数据类?
字段类型与声明它们的类型相同的对象层次结构需要一个小技巧来声明前向引用。
from typing import Optional
from dataclasses import dataclass
from dataclasses_json import dataclass_json
@dataclass_json
@dataclass
class Tree():
value: str
left: Optional['Tree']
right: Optional['Tree']
避免使用
from __future__ import annotations
因为它会导致dataclasses_json访问类型注解的方式出现问题。
如何使用numpy或pandas类型?
数据分析和机器学习中常用库如numpy和pandas的特定数据类型默认不支持,但你可以通过使用自定义解码器和编码器轻松启用它们。以下是numpy和pandas类型的两个示例。
from dataclasses import field, dataclass
from dataclasses_json import config, dataclass_json
import numpy as np
import pandas as pd
@dataclass_json
@dataclass
class DataWithNumpy:
my_int: np.int64 = field(metadata=config(decoder=np.int64))
my_float: np.float64 = field(metadata=config(decoder=np.float64))
my_array: np.ndarray = field(metadata=config(decoder=np.asarray))
DataWithNumpy.from_json("{\"my_int\": 42, \"my_float\": 13.37, \"my_array\": [1,2,3]}")
@dataclass_json
@dataclass
class DataWithPandas:
my_df: pd.DataFrame = field(metadata=config(decoder=pd.DataFrame.from_records, encoder=lambda x: x.to_dict(orient="records")))
data = DataWithPandas.from_dict({"my_df": [{"col1": 1, "col2": 2}, {"col1": 3, "col2": 4}]})
# my_df 结果为:
# col1 col2
# 1 2
# 3 4
data.to_dict()
# {"my_df": [{"col1": 1, "col2": 2}, {"col1": 3, "col2": 4}]}
Marshmallow互操作性
使用dataclass_json
装饰器或混入DataClassJsonMixin
将为你提供一个额外的方法.schema()
。
.schema()
生成的模式与手动为你的数据类创建marshmallow模式完全等价。你可以参考marshmallow API文档
了解使用.schema()
返回的模式的其他方法。
你可以向.schema()
传递与构造PersonSchema
实例时完全相同的参数,例如.schema(many=True)
,它们将被传递给marshmallow模式。
from dataclasses import dataclass
from dataclasses_json import dataclass_json
@dataclass_json
@dataclass
class Person:
name: str
# 你不需要这样做 - `.schema()`会为你生成这个!
from marshmallow import Schema, fields
class PersonSchema(Schema):
name = fields.Str()
简要说明上述示例中的内部工作原理:调用.schema()
将使本库为你生成一个
marshmallow模式。
它还填充了相应的对象钩子,以便marshmallow在load
时创建你的数据类的实例(例如
Person.schema().load
返回一个Person
),而不是默认的dict
。
性能说明
.schema()
没有缓存(它在每次调用时生成模式),所以如果你有一个嵌套的数据类,
你可能想将结果保存到一个变量中,以避免在每次使用时重新生成模式。
person_schema = Person.schema()
person_schema.dump(people, many=True)
# 代码中的稍后位置...
person_schema.dump(person)
覆盖/扩展
覆盖
例如,你可能想使用ISO格式而不是默认的timestamp
来编码/解码datetime
对象。
from dataclasses import dataclass, field
from dataclasses_json import dataclass_json, config
from datetime import datetime
from marshmallow import fields
@dataclass_json
@dataclass
class DataClassWithIsoDatetime:
created_at: datetime = field(
metadata=config(
encoder=datetime.isoformat,
decoder=datetime.fromisoformat,
mm_field=fields.DateTime(format='iso')
)
)
扩展
类似地,你可能想扩展dataclasses_json
以编码date
对象。
from dataclasses import dataclass, field
from dataclasses_json import dataclass_json, config
from datetime import date
from marshmallow import fields
dataclasses_json.cfg.global_config.encoders[date] = date.isoformat
dataclasses_json.cfg.global_config.decoders[date] = date.fromisoformat
@dataclass_json
@dataclass
class DataClassWithIsoDatetime:
created_at: date
modified_at: date
accessed_at: date
如你所见,你可以通过提供可调用的"钩子"来覆盖或扩展默认编解码器:
encoder
:一个可调用对象,在编码为JSON时将被调用以转换字段值decoder
:一个可调用对象,在从JSON解码时将被调用以转换JSON值mm_field
:一个marshmallow字段,它将影响涉及.schema()
的任何操作的行为
请注意,无论你是使用.to_json
/dump
/dumps
还是.from_json
/load
/loads
,这些钩子都将被调用。
因此,要谨慎应用覆盖/扩展,确保仔细考虑编码器/解码器/mm_field的交互是否符合你的预期!
如果我有其他依赖metadata
的数据类字段扩展怎么办?
dataclasses_json.config
所做的只是返回一个映射,命名空间位于键'dataclasses_json'
下。
假设有另一个模块other_dataclass_package
使用元数据。以下是解决问题的方法:
metadata = {'other_dataclass_package': '一些元数据...'} # 另一个数据类包的预先存在的元数据
dataclass_json_config = config(
encoder=datetime.isoformat,
decoder=datetime.fromisoformat,
mm_field=fields.DateTime(format='iso')
)
metadata.update(dataclass_json_config)
@dataclass_json
@dataclass
class DataClassWithIsoDatetime:
created_at: datetime = field(metadata=metadata)
你也可以手动指定dataclass_json配置映射。
@dataclass_json
@dataclass
class DataClassWithIsoDatetime:
created_at: date = field(
metadata={'dataclasses_json': {
'encoder': date.isoformat,
'decoder': date.fromisoformat,
'mm_field': fields.DateTime(format='iso')
}}
)
一个更大的示例
from dataclasses import dataclass
from dataclasses_json import dataclass_json
from typing import List
@dataclass_json
@dataclass(frozen=True)
class Minion:
name: str
@dataclass_json
@dataclass(frozen=True)
class Boss:
minions: List[Minion]
boss = Boss([Minion('邪恶的小黄人'), Minion('非常邪恶的小黄人')])
boss_json = """
{
"minions": [
{
"name": "邪恶的小黄人"
},
{
"name": "非常邪恶的小黄人"
}
]
}
""".strip()
assert boss.to_json(indent=4) == boss_json
assert Boss.from_json(boss_json) == boss
性能
请查看这个问题
版本控制
注意,该库仍处于1.0.0版本之前(SEMVER)。
当前的约定是:
- 补丁版本升级用于修复bug和添加小功能。
- 次要版本升级用于大型API功能和破坏性更改。
一旦该库达到1.0.0版本,将遵循标准的SEMVER约定。
Python兼容性
下表未列出的任何版本我们都不进行测试,尽管你可能仍然能够安装该库。对于未来的Python版本,请打开一个问题和/或拉取请求,将它们添加到CI套件中。
Python版本范围 | 兼容的dataclasses-json版本 |
---|---|
3.7.x - 3.12.x | 0.5.x - 0.6.x |
>= 3.13.x | 尚无官方支持 |
路线图
目前的重点是调查和修复这个库中的bug,改进性能,以及完成这个问题。
话虽如此,如果你认为库中缺少某个功能或需要新的东西,请参阅下面的贡献部分。
贡献
首先,感谢你有兴趣为这个库做出贡献。我真的很感谢你花时间在这个项目上。
- 如果你只是对代码感兴趣,一个好的起点是查看标记为bug的问题。
- 如果引入新功能,特别是修改公共API的功能,请考虑在提交PR之前提交一个问题进行讨论。也请查看现有的问题/PR,看看你提议的内容是否已经被涵盖或存在。
- 我喜欢遵循这里记录的提交约定
设置你的环境
本项目使用Poetry进行依赖和虚拟环境管理。准备好你的第一次提交非常简单:
- 安装最新稳定版的Poetry
- 导航到你克隆
dataclasses-json
的位置 - 运行
poetry install
- 创建一个分支并开始编写代码!