Сериализация члена Enum в JSON

96

Как сериализовать Enumчлен Python в JSON, чтобы я мог десериализовать полученный JSON обратно в объект Python?

Например, такой код:

from enum import Enum    
import json

class Status(Enum):
    success = 0

json.dumps(Status.success)

приводит к ошибке:

TypeError: <Status.success: 0> is not JSON serializable

Как мне этого избежать?

Билал Сайед Хуссейн
источник

Ответы:

52

Если вы хотите закодировать произвольный enum.Enumчлен в JSON, а затем декодировать его как тот же член перечисления (а не просто valueатрибут члена перечисления ), вы можете сделать это, написав собственный JSONEncoderкласс и функцию декодирования для передачи в качестве object_hookаргумента в json.load()или json.loads():

PUBLIC_ENUMS = {
    'Status': Status,
    # ...
}

class EnumEncoder(json.JSONEncoder):
    def default(self, obj):
        if type(obj) in PUBLIC_ENUMS.values():
            return {"__enum__": str(obj)}
        return json.JSONEncoder.default(self, obj)

def as_enum(d):
    if "__enum__" in d:
        name, member = d["__enum__"].split(".")
        return getattr(PUBLIC_ENUMS[name], member)
    else:
        return d

as_enumФункция опирается на JSON будучи закодирован с использованием EnumEncoder, или что - то, ведет себя тождественно ему.

Ограничение для членов PUBLIC_ENUMSнеобходимо, чтобы избежать использования злонамеренно созданного текста, например, для того, чтобы обмануть вызывающий код для сохранения личной информации (например, секретного ключа, используемого приложением) в несвязанном поле базы данных, откуда она может быть раскрыта. (см. http://chat.stackoverflow.com/transcript/message/35999686#35999686 ).

Пример использования:

>>> data = {
...     "action": "frobnicate",
...     "status": Status.success
... }
>>> text = json.dumps(data, cls=EnumEncoder)
>>> text
'{"status": {"__enum__": "Status.success"}, "action": "frobnicate"}'
>>> json.loads(text, object_hook=as_enum)
{'status': <Status.success: 0>, 'action': 'frobnicate'}
Нулевой Пирей
источник
1
Спасибо, Зеро! Хороший пример.
Итан Фурман
Если у вас есть код в модуле (например, enumencoder.py), вы должны импортировать класс, который вы разбираете, из JSON в dict. Например, в этом случае вы должны импортировать статус класса в модуль enumencoder.py.
Франсиско Мануэль Гарка Ботелла,
Меня беспокоил не вредоносный код вызова, а вредоносные запросы к веб-серверу. Как вы упомянули, личные данные могут быть представлены в ответе или могут использоваться для управления потоком кода. Спасибо, что обновили свой ответ. Было бы даже лучше, если бы основной пример кода был безопасным.
Джаред Декард
1
@JaredDeckard, мои извинения, вы были правы, а я ошибался. Я обновил ответ соответственно. Спасибо за ваш вклад! Это было поучительно (и поучительно).
Zero Piraeus
будет ли этот вариант более подходящим if isinstance(obj, Enum):?
user7440787
115

Я знаю, что это устарело, но чувствую, что это поможет людям. Я только что рассмотрел эту точную проблему и обнаружил, что если вы используете перечисления строк, объявление перечислений как подкласса strхорошо работает почти во всех ситуациях:

import json
from enum import Enum

class LogLevel(str, Enum):
    DEBUG = 'DEBUG'
    INFO = 'INFO'

print(LogLevel.DEBUG)
print(json.dumps(LogLevel.DEBUG))
print(json.loads('"DEBUG"'))
print(LogLevel('DEBUG'))

Выведет:

LogLevel.DEBUG
"DEBUG"
DEBUG
LogLevel.DEBUG

Как видите, загрузка JSON выводит строку, DEBUGно ее легко преобразовать обратно в объект LogLevel. Хороший вариант, если вы не хотите создавать собственный кодировщик JSONEncoder.

Джастин Картер
источник
1
Спасибо. Несмотря на то, что я в основном против множественного наследования, это довольно аккуратно, и я поступаю именно так. Дополнительный кодировщик не нужен :)
Винисиус Дантас
@madjardi, не могли бы вы подробнее рассказать о своей проблеме? У меня никогда не было проблем с тем, что значение строки отличается от имени атрибута в перечислении. Я неправильно понимаю ваш комментарий?
Джастин Картер
1
class LogLevel(str, Enum): DEBUG = 'Дебаг' INFO = 'Инфо'в этом случае enum with strне работает должным образом (
madjardi
1
Вы также можете проделать этот трюк, например, с другими базовыми типами (я не знаю, как отформатировать это в комментариях, но суть ясна: «class Shapes (int, Enum): square = 1 circle = 2» работает отличный вариант без кодировщика. Спасибо, это отличный подход!
NoCake
71

Правильный ответ зависит от того, что вы собираетесь делать с сериализованной версией.

Если вы собираетесь выполнить десериализацию обратно в Python, см . Ответ Zero .

Если ваша сериализованная версия будет переведена на другой язык, вы, вероятно, захотите использовать IntEnumвместо нее , которая автоматически сериализуется как соответствующее целое число:

from enum import IntEnum
import json

class Status(IntEnum):
    success = 0
    failure = 1

json.dumps(Status.success)

и это возвращается:

'0'
Итан Фурман
источник
5
@AShelly: Вопрос был помечен Python3.4, и этот ответ специфичен для 3.4+.
Итан Фурман
2
Отлично. Если Enum - это строка, вы должны использовать EnumMetaвместоIntEnum
bholagabbar
5
@bholagabbar: Нет, вы бы использовали Enum, возможно, с strмиксином -class MyStrEnum(str, Enum): ...
Итан Фурман
3
@bholagabbar, интересно. Вы должны опубликовать свое решение в качестве ответа.
Итан Фурман
1
Я бы избегал прямого наследования от EnumMeta, который был задуман только как метакласс. Вместо этого обратите внимание, что реализация IntEnum является однострочным, и вы можете добиться того же для strwith class StrEnum(str, Enum): ....
yungchin 04
15

В Python 3.7 можно просто использовать json.dumps(enum_obj, default=str)

кай
источник
Выглядит неплохо, но он запишет nameenum в строку json. Лучше всего использовать valueперечисление.
eNca,
Значение json.dumps(enum_obj, default=lambda x: x.value)
перечисления
10

Мне понравился ответ Zero Piraeus, но он немного изменил его для работы с API для Amazon Web Services (AWS), известным как Boto.

class EnumEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Enum):
            return obj.name
        return json.JSONEncoder.default(self, obj)

Затем я добавил этот метод в свою модель данных:

    def ToJson(self) -> str:
        return json.dumps(self.__dict__, cls=EnumEncoder, indent=1, sort_keys=True)

Я надеюсь, что это помогает кому-то.

Крендель
источник
Зачем вам нужно добавлять ToJsonв вашу модель данных?
Ю Чен
2

Если вы используете jsonpickleсамый простой способ, он должен выглядеть, как показано ниже.

from enum import Enum
import jsonpickle


@jsonpickle.handlers.register(Enum, base=True)
class EnumHandler(jsonpickle.handlers.BaseHandler):

    def flatten(self, obj, data):
        return obj.value  # Convert to json friendly format


if __name__ == '__main__':
    class Status(Enum):
        success = 0
        error = 1

    class SimpleClass:
        pass

    simple_class = SimpleClass()
    simple_class.status = Status.success

    json = jsonpickle.encode(simple_class, unpicklable=False)
    print(json)

После сериализации Json у вас будет, как ожидалось, {"status": 0}вместо

{"status": {"__objclass__": {"py/type": "__main__.Status"}, "_name_": "success", "_value_": 0}}
Рафалькаса
источник
-1

Это сработало для меня:

class Status(Enum):
    success = 0

    def __json__(self):
        return self.value

Больше ничего менять не пришлось. Очевидно, вы получите только значение из этого, и вам нужно будет проделать некоторую другую работу, если вы захотите позже преобразовать сериализованное значение обратно в перечисление.

ГерцогСеребряный
источник
2
Я не вижу ничего в документации, описывающей этот магический метод. Вы используете какую-то другую библиотеку JSON или у вас JSONEncoderгде-то есть собственный ?
0x5453