Project Icon

dataclasses-json

Python数据类与JSON转换的简化工具

dataclasses-json是一个为Python数据类提供JSON编码和解码功能的库。它支持嵌套数据类、多种Python集合类型和datetime对象,允许自定义字段名称和大小写。该库提供了处理缺失字段、未知字段和递归数据类的解决方案,简化了数据类与JSON之间的转换。这个工具对经常需要在Python数据类和JSON之间进行转换的开发者特别有用。

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

在解码时处理缺失或可选的字段值?

默认情况下,如果你的数据类中使用了defaultdefault_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]}
  1. 你可以通过将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
  1. 你可以通过将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"}}
  1. 你可以将它们保存在一个通用字段中,以便稍后进行处理。只需将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__会将参数解析为已定义的字段,并将其他所有内容写入通用字段。
  1. 这3个选项也适用于使用schema().loadsschema().dumps,只要你不通过指定schema(unknown=<marshmallow值>)来覆盖它。 marshmallow使用相同的3个关键字['include', 'exclude', 'raise']。

  2. 这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类型?

数据分析和机器学习中常用库如numpypandas的特定数据类型默认不支持,但你可以通过使用自定义解码器和编码器轻松启用它们。以下是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.x0.5.x - 0.6.x
>= 3.13.x尚无官方支持

路线图

目前的重点是调查和修复这个库中的bug,改进性能,以及完成这个问题

话虽如此,如果你认为库中缺少某个功能或需要新的东西,请参阅下面的贡献部分。

贡献

首先,感谢你有兴趣为这个库做出贡献。我真的很感谢你花时间在这个项目上。

  • 如果你只是对代码感兴趣,一个好的起点是查看标记为bug的问题。
  • 如果引入新功能,特别是修改公共API的功能,请考虑在提交PR之前提交一个问题进行讨论。也请查看现有的问题/PR,看看你提议的内容是否已经被涵盖或存在。
  • 我喜欢遵循这里记录的提交约定

设置你的环境

本项目使用Poetry进行依赖和虚拟环境管理。准备好你的第一次提交非常简单:

  • 安装最新稳定版的Poetry
  • 导航到你克隆dataclasses-json的位置
  • 运行poetry install
  • 创建一个分支并开始编写代码!
项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

AIWritePaper论文写作

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号