如何将 JSON 转换为具有类型信息和枚举的对象?

How to convert JSON to an object with type information and enums?

将一些 JSON 编组为强类型 Python 对象的最佳方法是什么,该对象提供更多关于数据的保证,而不是仅仅转换为类似 dict 的对象?

我有一些来自外部 API 的 JSON,看起来像这样:

{
    "Name of Event": {
        "start": "2021-01-01 00:00:00",
        "event_type": 1
    },
    "Another Event": {
        "start": "2021-01-01 00:00:00",
        "event_type": 2,
    }
}

(重要的是要知道我正在使用的实际 JSON 要复杂得多(并且嵌套很深),但基本上它是一个结构良好的东西,一直到已知类型。 )

我可以做一些简单的事情:

for name, event in json.loads(data):
     do_things(name, event['start'], event['event_type'])

但这感觉相当混乱,并且在写入时或 运行 时并没有给我的程序太多的类型检查。

在我处理这个的代码中 JSON 我想使用正确类型的东西。但我不想写一大堆样板文件。

我可以做一些非常明确的事情,比如:

DATE_FORMAT = "%y-%m-%d %H:%M:%S"

class EventType:
    FREE_FOR_ALL = 1
    CLOSED_REGISTRATION = 2

class Event:
    __slots__ = ["start", "event_type"]

    start: datetime.datetime
    event_type: EventType

    def __init__(self, start, event_type):
        self.start = datetime.datetime.strptime(start, DATE_FORMAT)
        self.event_type = EventType(event_type)

    def __str__(self):
        return str(self.__dict__)

    def __repr__(self):
        return str(self.__dict__)

APIResponse = Dict[str, Event]

for name, raw_event in data:
    event = Event(**data)
    do_things(name, event)

就目前而言,这很好,但是一旦您有十几个 classes,每个属性都有十几个属性,它就会开始看起来像很多样板。特别是我觉得我定义了每个 属性 两次,违反了 DRY。一次在 class 一次在 __init__.

(我也有点担心这对于 API 在任何给定枚举中添加新选项等情况来说有点“脆弱”,但正如我所期望的那样,这是一个较小的问题API 更改以要求更改我的代码。)

我想知道是否有任何我可以使用的魔法可以做到这一点所以我只需要在一个地方定义每个字段但仍然可以进行良好的类型检查并且 运行时间保证数据在我期望的形式?

我查看了 dataclasses,但似乎我无法干扰 JSON 的简单 string/int 输入到枚举、日期时间等。我可以使用 InitVar 将大量输入标记为“仅 __init__”,然后使用 __post_init__ 使用我的数据的强类型版本填充具有不同名称的其他字段。但是大多数这些字段的“正确”名称是它在 JSON 中已有的名称(我不想写 event.event_type_typed_version)。

理想情况下我会这样写:

@magic_annotation
class Event:
    start: datetime.datetime
    event_type: EventType

for name, raw_event in data:
    event = Event(**data)
    do_things(name, event)

并且不需要其他任何东西。 magic_annotation 存在吗?有没有完全不同的方法来解决这个问题?

听起来你在追求 pydantic

from datetime import datetime
from enum import Enum
from pydantic import BaseModel

class EventType(Enum):
    FREE_FOR_ALL = 1
    CLOSED_REGISTRATION = 2


class Event(BaseModel):
    start: datetime
    event_type: EventType

event = Event.parse_obj({
    "start": "2021-01-01 00:00:00",
    "event_type": 1
})

print(repr(event))
# Event(start=datetime.datetime(2021, 1, 1, 0, 0), event_type=<EventType.FREE_FOR_ALL: 1>)

Pydantic 会根据 类 属性的类型注释自动转换您的输入。对于日期时间,它支持标准的 ISO 8601 格式。对于枚举,它会自动从枚举值转换。 Check it out!(非附属,只是粉丝。)