将 Python 枚举编码为 JSON

Encoding Python Enum to JSON

我有一本字典,其中一些键是枚举实例(enum.Enum 的子classes)。我正在尝试根据 documentation 使用自定义 JSON 编码器 class 将字典编码为 JSON 字符串。我想要的只是让输出的 JSON 中的键成为枚举名称的字符串。例如 { TestEnum.one : somevalue } 将被编码为 { "one" : somevalue }.

我写了一个简单的测试用例,如下所示,我在干净的 virtualenv 中测试过:

import json

from enum import Enum

class TestEnum(Enum):
    one = "first"
    two = "second"
    three = "third"

class TestEncoder(json.JSONEncoder):
    """ Custom encoder class """

    def default(self, obj):

        print("Default method called!")

        if isinstance(obj, TestEnum):
            print("Seen TestEnum!")
            return obj.name

        return json.JSONEncoder.default(self, obj)

def encode_enum(obj):
    """ Custom encoder method """

    if isinstance(obj, TestEnum):
        return obj.name
    else:
        raise TypeError("Don't know how to decode this")

if __name__ == "__main__":

    test = {TestEnum.one : "This",
            TestEnum.two : "should",
            TestEnum.three : "work!"}

    # Test dumps with Encoder method
    #print("Test with encoder method:")
    #result = json.dumps(test, default=encode_enum)
    #print(result)

    # Test dumps with Encoder Class
    print("Test with encoder class:")
    result = json.dumps(test, cls=TestEncoder)
    print(result)

我无法成功对字典进行编码(使用 Python 3.6.1)。我不断收到 TypeError: keys must be a string 错误,我的自定义编码器实例的默认方法(通过 json.dumps 方法的 cls 参数提供)似乎从未被调用过?我还尝试通过 json.dumps 方法的 default 参数提供自定义编码方法,但同样从未触发。

我见过涉及 IntEnum 的解决方案 class,但我需要 Enum 的值是字符串。我还看到 this answer 讨论了与从另一个 class 继承的枚举相关的问题。但是,我的枚举仅从基础 enum.Enum class 继承并正确响应 isinstance 调用?

自定义 class 和方法在提供给 json.dumps 方法时都会生成 TypeError。典型输出如下所示:

$ python3 enum_test.py

Test with encoder class
Traceback (most recent call last):
  File "enum_test.py", line 59, in <module>
    result = json.dumps(test, cls=TestEncoder)
  File "/usr/lib64/python3.6/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "/usr/lib64/python3.6/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib64/python3.6/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
TypeError: keys must be a string

我认为问题是 JSONEncoder class 的 encode 方法假定它知道如何序列化 Enum class(因为其中一个如果 iterencode 方法中的语句被触发),因此从不调用自定义默认方法并以序列化枚举失败而告终?

如有任何帮助,我们将不胜感激!

除了 字符串 之外,您不能将任何其他内容用作要转换为 JSON 的字典中的键。编码器没有给你任何其他选择; default 钩子只为未知类型的值调用,从不为键调用。

预先将您的密钥转换为字符串:

def convert_keys(obj, convert=str):
    if isinstance(obj, list):
        return [convert_keys(i, convert) for i in obj]
    if not isinstance(obj, dict):
        return obj
    return {convert(k): convert_keys(v, convert) for k, v in obj.items()}

json.dumps(convert_keys(test))

这会递归地处理您的字典键。请注意,我包括了一个钩子;然后您可以选择如何将枚举值转换为字符串:

def enum_names(key):
    if isinstance(key, TestEnum):
        return key.name
    return str(key)

json.dumps(convert_keys(test, enum_names))

从 JSON:

加载时,您可以使用相同的函数来反转过程
def names_to_enum(key):
    try:
        return TestEnum[key]
    except KeyError:
        return key

convert_keys(json.loads(json_data), names_to_enum)

演示:

>>> def enum_names(key):
...     if isinstance(key, TestEnum):
...         return key.name
...     return str(key)
...
>>> json_data = json.dumps(convert_keys(test, enum_names))
>>> json_data
'{"one": "This", "two": "should", "three": "work!"}'
>>> def names_to_enum(key):
...     try:
...         return TestEnum[key]
...     except KeyError:
...         return key
...
>>> convert_keys(json.loads(json_data), names_to_enum)
{<TestEnum.one: 'first'>: 'This', <TestEnum.two: 'second'>: 'should', <TestEnum.three: 'third'>: 'work!'}

我不再使用内置的 python 枚举,我使用一个名为 "TypedEnum" 的元类。

原因是元类允许我的字符串枚举像字符串一样工作:它们可以传递给接受字符串的函数,它们可以序列化为字符串(就像你想要的......就在 JSON编码),但它们仍然是强类型(isA Enum)。

https://gist.github.com/earonesty/81e6c29fa4c54e9b67d9979ddbd8489d

我 运行 使用常规枚举遇到的奇怪错误的数量是无法计数的。

class TypedEnum(type):
    """This metaclass creates an enumeration that preserve isinstance(element, type)."""

    def __new__(mcs, cls, _bases, classdict):
        """Discover the enum members by removing all intrinsics and specials."""
        object_attrs = set(dir(type(cls, (object,), {})))
        member_names = set(classdict.keys()) - object_attrs
        member_names = member_names - set(name for name in member_names if name.startswith('_') and name.endswith('_'))
        new_class = None
        base = None
        for attr in member_names:
            value = classdict[attr]
            if new_class is None:
                # base class for all members is the type of the value
                base = type(classdict[attr])
                new_class = super().__new__(mcs, cls, (base, ), classdict)
                setattr(new_class, "__member_names__", member_names)
            else:
                if not base == type(classdict[attr]):           # noqa
                    raise SyntaxError("Cannot mix types in TypedEnum")
            setattr(new_class, attr, new_class(value))

        return new_class

    def __call__(cls, arg):
        for name in cls.__member_names__:
            if arg == getattr(cls, name):
                return type.__call__(cls, arg)
        raise ValueError("Invalid value '%s' for %s" % (arg, cls.__name__))

    def __iter__(cls):
        """List all enum values."""
        return (getattr(cls, name) for name in cls.__member_names__)

    def __len__(cls):
        """Get number of enum values."""
        return len(cls.__member_names__)

这是一个老问题。但是没有人给出这么简单的答案。

你只需要从 str.

继承你的枚举
import json

from enum import Enum

class TestEnum(str, Enum):
    one = "first"
    two = "second"
    three = "third"

test = {TestEnum.one : "This",
        TestEnum.two : "should",
        TestEnum.three : "work!"}

print(json.dumps(test))

输出:

{"first": "This", "second": "should", "third": "work!"}