Serialisierung eines Enum-Mitglieds für JSON

86

Wie serialisiere ich ein Python- EnumMitglied in JSON, damit ich das resultierende JSON wieder in ein Python-Objekt deserialisieren kann?

Zum Beispiel dieser Code:

from enum import Enum    
import json

class Status(Enum):
    success = 0

json.dumps(Status.success)

führt zu dem Fehler:

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

Wie kann ich das vermeiden?

Bilal Syed Hussain
quelle

Antworten:

49

Wenn Sie ein beliebiges enum.EnumMitglied in JSON codieren und es dann als dasselbe Aufzählungselement (und nicht nur als valueAttribut des Aufzählungsmitglieds ) dekodieren möchten , können Sie dies tun, indem Sie eine benutzerdefinierte JSONEncoderKlasse und eine Dekodierungsfunktion schreiben , die als object_hookArgument an json.load()oder übergeben wird 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

Die as_enumFunktion basiert darauf, dass der JSON mit codiert EnumEncoderwurde oder etwas, das sich identisch mit ihm verhält.

Die Beschränkung auf Mitglieder von PUBLIC_ENUMSist erforderlich, um zu verhindern, dass ein böswillig gestalteter Text verwendet wird, um beispielsweise den Aufruf von Code zum Speichern privater Informationen (z. B. eines von der Anwendung verwendeten geheimen Schlüssels) in einem nicht verwandten Datenbankfeld zu betrügen, von wo aus sie dann verfügbar gemacht werden könnten (Siehe http://chat.stackoverflow.com/transcript/message/35999686#35999686 ).

Anwendungsbeispiel:

>>> 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'}
Null Piräus
quelle
1
Danke, Zero! Schönes Beispiel.
Ethan Furman
Wenn Sie Ihren Code in einem Modul haben (z. B. enumencoder.py), müssen Sie die Klasse, die Sie analysieren, aus JSON importieren, um sie zu diktieren. In diesem Fall müssen Sie beispielsweise die Klasse Status in das Modul enumencoder.py importieren.
Francisco Manuel Garca Botella
Mein Anliegen war nicht böswilliger Anrufcode, sondern böswillige Anfragen an einen Webserver. Wie Sie bereits erwähnt haben, können die privaten Daten in einer Antwort verfügbar gemacht oder zum Manipulieren des Codeflusses verwendet werden. Vielen Dank für die Aktualisierung Ihrer Antwort. Es wäre sogar noch besser, wenn das Hauptcodebeispiel sicher wäre.
Jared Deckard
1
@ JaredDeckard Ich entschuldige mich, Sie hatten Recht und ich habe mich geirrt. Ich habe die Antwort entsprechend aktualisiert. Danke für deinen Beitrag! Dies war lehrreich (und züchtigend).
Zero Piraeus
Wäre diese Option angemessener if isinstance(obj, Enum):?
user7440787
101

Ich weiß, dass dies alt ist, aber ich denke, dass dies den Menschen helfen wird. Ich habe gerade genau dieses Problem durchgearbeitet und festgestellt, ob Sie Zeichenfolgenaufzählungen verwenden, und Ihre Aufzählungen als Unterklasse von strArbeiten deklariert, die für fast alle Situationen gut geeignet sind :

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

Wird ausgegeben:

LogLevel.DEBUG
"DEBUG"
DEBUG
LogLevel.DEBUG

Wie Sie sehen können, gibt das Laden des JSON die Zeichenfolge aus DEBUG, kann jedoch problemlos wieder in ein LogLevel-Objekt umgewandelt werden. Eine gute Option, wenn Sie keinen benutzerdefinierten JSONEncoder erstellen möchten.

Justin Carter
quelle
Vielen Dank. Obwohl ich meistens gegen Mehrfachvererbungen bin, ist das ziemlich ordentlich und so gehe ich vor. Kein zusätzlicher Encoder erforderlich :)
Vinicius Dantas
@madjardi, können Sie das Problem, das Sie haben, näher erläutern? Ich hatte noch nie ein Problem damit, dass der Wert der Zeichenfolge vom Namen des Attributs in der Aufzählung abweicht. Verstehe ich Ihren Kommentar falsch?
Justin Carter
1
class LogLevel(str, Enum): DEBUG = 'Дебаг' INFO = 'Инфо'in diesem Fall enum with strkeine Arbeit richtig (
Madjardi
65

Die richtige Antwort hängt davon ab, was Sie mit der serialisierten Version tun möchten.

Wenn Sie wieder in Python unserialisieren möchten, lesen Sie die Antwort von Zero .

Wenn Ihre serialisierte Version in eine andere Sprache wechselt, möchten Sie wahrscheinlich IntEnumstattdessen eine verwenden, die automatisch als entsprechende Ganzzahl serialisiert wird:

from enum import IntEnum
import json

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

json.dumps(Status.success)

und das kehrt zurück:

'0'
Ethan Furman
quelle
5
@AShelly: Die Frage wurde mit markiert Python3.4und diese Antwort ist 3.4+ spezifisch.
Ethan Furman
2
Perfekt. Wenn Sie Enum eine Zeichenfolge ist, würden Sie EnumMetaanstelle vonIntEnum
bholagabbar
5
@bholagabbar: Nein, würden Sie verwenden Enum, möglicherweise mit einem strMixin -class MyStrEnum(str, Enum): ...
Ethan Furman
3
@bholagabbar, interessant. Sie sollten Ihre Lösung als Antwort veröffentlichen.
Ethan Furman
1
Ich würde es vermeiden, direkt von zu erben EnumMeta, was nur als Metaklasse gedacht war. Beachten Sie stattdessen, dass die Implementierung von IntEnum ein Einzeiler ist und Sie dasselbe für strmit erreichen können class StrEnum(str, Enum): ....
Yungchin
13

In Python 3.7 kann nur verwendet werden json.dumps(enum_obj, default=str)

kai
quelle
10

Ich mochte die Antwort von Zero Piraeus, änderte sie jedoch geringfügig, um mit der API für Amazon Web Services (AWS), bekannt als Boto, zu arbeiten.

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

Ich habe dann diese Methode zu meinem Datenmodell hinzugefügt:

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

Ich hoffe das hilft jemandem.

Brezel
quelle
0

Wenn Sie jsonpickleden einfachsten Weg verwenden, sollten Sie wie folgt aussehen.

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)

Nach der Json-Serialisierung haben Sie wie erwartet {"status": 0}statt

{"status": {"__objclass__": {"py/type": "__main__.Status"}, "_name_": "success", "_value_": 0}}
Rafalkasa
quelle
-1

Das hat bei mir funktioniert:

class Status(Enum):
    success = 0

    def __json__(self):
        return self.value

Musste nichts anderes ändern. Offensichtlich erhalten Sie den Wert nur daraus und müssen andere Arbeiten ausführen, wenn Sie den serialisierten Wert später wieder in die Aufzählung konvertieren möchten.

DukeSilver
quelle
Ich sehe nichts in den Dokumenten , die diese magische Methode beschreiben. Verwenden Sie eine andere JSON-Bibliothek oder haben Sie JSONEncoderirgendwo eine benutzerdefinierte Bibliothek ?
0x5453