Serializacja elementu członkowskiego Enum do formatu JSON

99

Jak serializować element Enumczłonkowski języka Python do formatu JSON, aby móc deserializować wynikowy kod JSON z powrotem do obiektu w języku Python?

Na przykład ten kod:

from enum import Enum    
import json

class Status(Enum):
    success = 0

json.dumps(Status.success)

powoduje błąd:

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

Jak mogę tego uniknąć?

Bilal Syed Hussain
źródło

Odpowiedzi:

54

Jeśli chcesz zakodować dowolny enum.Enumelement członkowski do formatu JSON, a następnie zdekodować go jako ten sam element członkowski wyliczenia (zamiast po prostu valueatrybut elementu członkowskiego wyliczenia ), możesz to zrobić, pisząc JSONEncoderklasę niestandardową i funkcję dekodującą, która ma zostać przekazana jako object_hookargument do json.load()lub 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_enumFunkcja polega na JSON został zakodowany z użyciem EnumEncoder, albo coś, co zachowuje się identycznie do niego.

Ograniczenie do członków PUBLIC_ENUMSjest konieczne, aby uniknąć wykorzystania złośliwie spreparowanego tekstu w celu, na przykład, oszukania wywołującego kodu do zapisania prywatnych informacji (np. Tajnego klucza używanego przez aplikację) w niepowiązanym polu bazy danych, skąd mógłby zostać ujawniony (patrz http://chat.stackoverflow.com/transcript/message/35999686#35999686 ).

Przykładowe użycie:

>>> 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'}
Zero Piraeus
źródło
1
Dzięki, Zero! Niezły przykład.
Ethan Furman
Jeśli masz kod w module (na przykład enumencoder.py), musisz zaimportować klasę, którą analizujesz z JSON, aby dyktować. Na przykład w tym przypadku musisz zaimportować klasę Status do modułu enumencoder.py.
Francisco Manuel Garca Botella
Nie martwiłem się o złośliwy kod wywołujący, ale złośliwe żądania do serwera WWW. Jak wspomniałeś, prywatne dane mogą zostać ujawnione w odpowiedzi lub mogą zostać użyte do manipulowania przepływem kodu. Dziękujemy za zaktualizowanie odpowiedzi. Byłoby jeszcze lepiej, gdyby główny przykład kodu był bezpieczny.
Jared Deckard
1
@JaredDeckard przepraszam, miałeś rację, a ja się myliłem. Odpowiednio zaktualizowałem odpowiedź. Dzięki za wkład! To było pouczające (i karcące).
Zero Piraeus
czy ta opcja byłaby bardziej odpowiednia if isinstance(obj, Enum):?
user7440787
127

Wiem, że to stare, ale czuję, że to pomoże ludziom. Właśnie przejrzałem ten dokładny problem i odkryłem, że jeśli używasz wyliczeń łańcuchowych, deklarowanie wyliczeń jako podklasy strdziała dobrze w prawie wszystkich sytuacjach:

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'))

Wyświetli:

LogLevel.DEBUG
"DEBUG"
DEBUG
LogLevel.DEBUG

Jak widać, ładowanie JSON generuje ciąg, DEBUGale można go łatwo przesłać z powrotem do obiektu LogLevel. Dobra opcja, jeśli nie chcesz tworzyć niestandardowego JSONEncodera.

Justin Carter
źródło
1
Dzięki. Chociaż jestem głównie przeciwny wielokrotnemu dziedziczeniu, to całkiem fajne i tak właśnie postępuję. Nie potrzeba dodatkowego kodera :)
Vinicius Dantas
@madjardi, czy możesz wyjaśnić problem, który masz? Nigdy nie miałem problemu z wartością ciągu różniącą się od nazwy atrybutu w wyliczeniu. Czy źle zrozumiałem twój komentarz?
Justin Carter
1
class LogLevel(str, Enum): DEBUG = 'Дебаг' INFO = 'Инфо'w tym przypadku enum with strnie działa poprawnie (
madjardi
1
Możesz także zrobić tę sztuczkę z innymi typami podstawowymi, na przykład (nie wiem jak sformatować to w komentarzach, ale sedno jest jasne: "class Shapes (int, Enum): square = 1 circle = 2" działa świetne bez kodera. Dzięki, to świetne podejście!
NoCake
Działa jak urok, dzięki! Powinien zostać zaakceptowany jako odpowiedź.
Realfun
72

Prawidłowa odpowiedź zależy od tego, co zamierzasz zrobić z wersją serializowaną.

Jeśli zamierzasz cofnąć serializację z powrotem do Pythona, zobacz odpowiedź Zero .

Jeśli twoja wersja serializowana jest przenoszona do innego języka, prawdopodobnie chcesz użyć IntEnumzamiast niej, który jest automatycznie serializowany jako odpowiednia liczba całkowita:

from enum import IntEnum
import json

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

json.dumps(Status.success)

a to zwraca:

'0'
Ethan Furman
źródło
5
@AShelly: Pytanie zostało oznaczone tagiem Python3.4, a ta odpowiedź dotyczy wersji 3.4+.
Ethan Furman,
2
Doskonały. Jeśli Enum jest łańcuchem znaków, użyjesz EnumMetazamiastIntEnum
bholagabbar,
5
@bholagabbar: Nie, użyłbyś Enum, prawdopodobnie z strmiksem -class MyStrEnum(str, Enum): ...
Ethan Furman
3
@bholagabbar, ciekawe. W odpowiedzi powinieneś zamieścić swoje rozwiązanie.
Ethan Furman
1
Unikałbym dziedziczenia bezpośrednio z EnumMeta, które było przeznaczone tylko jako metaklasa. Zamiast tego zauważ, że implementacja IntEnum jest jednolinijkowa i możesz osiągnąć to samo za strpomocą class StrEnum(str, Enum): ....
yungchin,
18

W Pythonie 3.7 można po prostu użyć json.dumps(enum_obj, default=str)

kai
źródło
Wygląda ładnie, ale zapisze nameenum w łańcuchu json. Lepszym sposobem będzie użycie valuewyliczenia.
eNca
Wartość wyliczenia może zostać użyta dojson.dumps(enum_obj, default=lambda x: x.value)
eNca
10

Podobała mi się odpowiedź Zero Piraeus, ale zmodyfikowałem ją nieco do pracy z API dla Amazon Web Services (AWS) znanym jako Boto.

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

Następnie dodałem tę metodę do mojego modelu danych:

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

Mam nadzieję, że to komuś pomoże.

Precel
źródło
Dlaczego musisz dodać ToJsondo swojego modelu danych?
Yu Chen
2

Jeśli używasz jsonpicklenajłatwiejszego sposobu, powinieneś wyglądać jak poniżej.

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)

Po serializacji Json będziesz mieć zgodnie z oczekiwaniami {"status": 0}zamiast

{"status": {"__objclass__": {"py/type": "__main__.Status"}, "_name_": "success", "_value_": 0}}
rafalkasa
źródło
-2

To zadziałało dla mnie:

class Status(Enum):
    success = 0

    def __json__(self):
        return self.value

Nie musiałem niczego zmieniać. Oczywiście uzyskasz tylko wartość z tego i będziesz musiał wykonać inną pracę, jeśli chcesz później przekonwertować zserializowaną wartość z powrotem na wyliczenie.

DukeSilver
źródło
2
Nie widzę niczego w dokumentach opisujących tę magiczną metodę. Czy korzystasz z innej biblioteki JSON, czy masz JSONEncodergdzieś niestandardową ?
0x5453