Кодирование Python Enum в JSON

У меня есть словарь, где некоторые ключи являются экземплярами Enum (подклассы enum.Enum). Я пытаюсь закодировать словарь в строку JSON, используя пользовательский класс JSON Encoder согласно документации. Все, что я хочу, это чтобы ключи в выводимом JSON были строками имен Enum. Например { 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 ошибки и метод по умолчанию для моего экземпляра пользовательского кодировщика (предоставляется через cls аргумент json.dumps метод) никогда не называется? Я также попытался предоставить собственный метод кодирования через default аргумент json.dumps метод, но опять же это никогда не срабатывает.

Я видел решения, включающие класс IntEnum, но мне нужно, чтобы значения Enum были строками. Я также видел этот ответ, в котором обсуждается проблема, связанная с Enum, которая наследуется от другого класса. Однако мои перечисления наследуются от базового класса enum.Enum только и правильно отвечают на isinstance звонки?

И пользовательский класс, и метод создают TypeError при подаче на json.dumps метод. Типичный вывод показан ниже:

$ 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

Я предполагаю, что проблема в том, что encode метод класса JSONEncoder предполагает, что он знает, как сериализовать класс Enum (потому что один из операторов if в iterencode метод запущен) и поэтому никогда не вызывает пользовательских методов по умолчанию и завершается с ошибкой сериализации Enum?

Любая помощь будет принята с благодарностью!

1 ответ

Решение

Вы не можете использовать ничего, кроме строк, в качестве ключей в словарях, которые вы хотите преобразовать в JSON. Кодировщик не дает вам никаких других опций; default hook вызывается только для значений неизвестного типа, но не для ключей.

Конвертируйте ваши ключи в строки:

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!'}

Это старый вопрос. Но никто не дал такого простого ответа.

Вам просто нужно создать подкласс Enum от 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!"}

Я больше не использую встроенное перечисление 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__)
Другие вопросы по тегам