Serializar un miembro de Enum en JSON

96

¿Cómo serializo un Enummiembro de Python en JSON, de modo que pueda deserializar el JSON resultante de nuevo en un objeto de Python?

Por ejemplo, este código:

from enum import Enum    
import json

class Status(Enum):
    success = 0

json.dumps(Status.success)

da como resultado el error:

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

¿Cómo puedo evitar eso?

Bilal Syed Hussain
fuente

Respuestas:

52

Si desea codificar un enum.Enummiembro arbitrario en JSON y luego decodificarlo como el mismo miembro de enumeración (en lugar de simplemente el valueatributo del miembro de enumeración ), puede hacerlo escribiendo una JSONEncoderclase personalizada y una función de decodificación para pasar como object_hookargumento json.load()ao 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

La as_enumfunción se basa en que el JSON se haya codificado usando EnumEncoder, o algo que se comporte de manera idéntica.

La restricción a los miembros de PUBLIC_ENUMSes necesaria para evitar que se utilice un texto creado con fines malintencionados para, por ejemplo, engañar al código de llamada para que guarde información privada (por ejemplo, una clave secreta utilizada por la aplicación) en un campo de base de datos no relacionado, desde donde podría quedar expuesto. (ver http://chat.stackoverflow.com/transcript/message/35999686#35999686 ).

Uso de ejemplo:

>>> 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
fuente
1
¡Gracias, Zero! Buen ejemplo.
Ethan Furman
Si tiene su código en un módulo (enumencoder.py, por ejemplo), debe importar la clase que analiza de JSON para dict. Por ejemplo, en este caso, debe importar la clase Status en el módulo enumencoder.py.
Francisco Manuel Garca Botella
Mi preocupación no era el código de llamada malicioso, sino las solicitudes maliciosas a un servidor web. Como mencionó, los datos privados podrían exponerse en una respuesta o podrían usarse para manipular el flujo de código. Gracias por actualizar tu respuesta. Sin embargo, sería aún mejor si el ejemplo de código principal fuera seguro.
Jared Deckard
1
@JaredDeckard mis disculpas, tenías razón y yo estaba equivocado. Actualicé la respuesta en consecuencia. ¡Gracias por tu contribución! Esto ha sido educativo (y castigador).
Zero Piraeus
¿Sería más apropiada esta opción if isinstance(obj, Enum):?
user7440787
114

Sé que esto es antiguo, pero creo que ayudará a la gente. Acabo de pasar por este problema exacto y descubrí que si está usando enumeraciones de cadena, declarar sus enumeraciones como una subclase de strfunciona bien para casi todas las situaciones:

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

Saldrá:

LogLevel.DEBUG
"DEBUG"
DEBUG
LogLevel.DEBUG

Como puede ver, al cargar JSON se genera la cadena, DEBUGpero se puede convertir fácilmente en un objeto LogLevel. Una buena opción si no desea crear un JSONEncoder personalizado.

Justin Carter
fuente
1
Gracias. Aunque estoy mayoritariamente en contra de las herencias múltiples, eso es bastante bueno y así es como voy. No se necesita codificador adicional :)
Vinicius Dantas
@madjardi, ¿puedes explicarnos el problema que tienes? Nunca he tenido un problema con que el valor de la cadena sea diferente al nombre del atributo en la enumeración. ¿Estoy entendiendo mal tu comentario?
Justin Carter
1
class LogLevel(str, Enum): DEBUG = 'Дебаг' INFO = 'Инфо'en este caso enum with strno funciona correctamente (
madjardi
1
También puede hacer este truco con otros tipos de base, por ejemplo (no sé cómo formatear esto en los comentarios, pero la esencia es clara: "class Shapes (int, Enum): cuadrado = 1 círculo = 2" funciona genial sin necesidad de un codificador. Gracias, ¡este es un gran enfoque!
NoCake
71

La respuesta correcta depende de lo que pretenda hacer con la versión serializada.

Si va a anular la serialización de nuevo en Python, consulte la respuesta de Zero .

Si su versión serializada va a otro idioma, probablemente desee usar un IntEnumen su lugar, que se serializa automáticamente como el número entero correspondiente:

from enum import IntEnum
import json

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

json.dumps(Status.success)

y esto devuelve:

'0'
Ethan Furman
fuente
5
@AShelly: La pregunta fue etiquetada con Python3.4, y esta respuesta es 3.4+ específica.
Ethan Furman
2
Perfecto. Si su Enum es una cadena, usaría en EnumMetalugar deIntEnum
bholagabbar
5
@bholagabbar: No, lo usarías Enum, posiblemente con un strmixin -class MyStrEnum(str, Enum): ...
Ethan Furman
3
@bholagabbar, interesante. Debería publicar su solución como respuesta.
Ethan Furman
1
Evitaría heredar directamente de EnumMeta, lo que estaba destinado a ser una metaclase únicamente. En su lugar, tenga en cuenta que la implementación de IntEnum es de una sola línea y puede lograr lo mismo para strcon class StrEnum(str, Enum): ....
yungchin
15

En Python 3.7, solo puede usar json.dumps(enum_obj, default=str)

kai
fuente
Se ve bien, pero escribirá la nameenumeración de en la cadena json. La mejor forma será utilizar valuela enumeración.
eNca
El valor de json.dumps(enum_obj, default=lambda x: x.value)
enumeración
10

Me gustó la respuesta de Zero Piraeus, pero la modifiqué ligeramente para trabajar con la API para Amazon Web Services (AWS) conocida como Boto.

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

Luego agregué este método a mi modelo de datos:

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

Espero que esto ayude a alguien.

Galleta salada
fuente
¿Por qué necesita agregar ToJsona su modelo de datos?
Yu Chen
2

Si está utilizando jsonpicklela forma más sencilla debería verse como a continuación.

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)

Después de la serialización de Json, tendrá lo esperado en {"status": 0}lugar de

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

Esto funcionó para mí:

class Status(Enum):
    success = 0

    def __json__(self):
        return self.value

No tuve que cambiar nada más. Obviamente, solo obtendrá el valor de esto y tendrá que hacer algún otro trabajo si desea convertir el valor serializado nuevamente en la enumeración más adelante.

DukeSilver
fuente
2
No veo nada en los documentos que describa ese método mágico. ¿Está utilizando alguna otra biblioteca JSON o tiene una personalizada en JSONEncoderalguna parte?
0x5453