Серіалізація члена 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 для диктування. Наприклад, у цьому випадку потрібно імпортувати клас Status у модулі enumencoder.py.
Франциско Мануель Гарка Ботелла

Мене турбувало не зловмисний код виклику, а зловмисні запити до веб-сервера. Як ви вже згадували, приватні дані можуть бути виставлені у відповідь або використані для маніпулювання потоком коду. Дякуємо, що оновили свою відповідь. Було б навіть краще, якби головний приклад коду був надійним.
Джаред Декард,

1
@JaredDeckard мої вибачення, ви мали рацію, а я помилявся. Відповідь я оновив відповідно. Дякуємо за ваш внесок! Це було освітнім (і каральним).
Нульовий Пірей

чи був би цей варіант доречнішим 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
Ви також можете зробити цей фокус з іншими базовими типами, наприклад (я не знаю, як це форматувати в коментарях, але суть зрозуміла: працює "Форми класу (int, Enum): квадрат = 1 коло = 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 є однолінійною, і ви можете досягти того ж за strдопомогою class StrEnum(str, Enum): ....
yungchin

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 Pireeus, але я її дещо змінив для роботи з API для веб-служб Amazon (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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.