Serializando um membro Enum para JSON

99

Como faço para serializar um Enummembro Python para JSON, para que possa desserializar o JSON resultante de volta em um objeto Python?

Por exemplo, este código:

from enum import Enum    
import json

class Status(Enum):
    success = 0

json.dumps(Status.success)

resulta no erro:

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

Como posso evitar isso?

Bilal Syed Hussain
fonte

Respostas:

54

Se você deseja codificar um enum.Enummembro arbitrário para JSON e, em seguida, decodificá-lo como o mesmo membro enum (em vez de simplesmente o valueatributo do membro enum ), você pode fazer isso escrevendo uma JSONEncoderclasse personalizada e uma função de decodificação para passar como o object_hookargumento parajson.load() ou 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

A as_enumfunção depende do JSON ter sido codificado usando EnumEncoder, ou algo que se comporta de forma idêntica a ele.

A restrição a membros de PUBLIC_ENUMS é necessária para evitar que um texto elaborado com códigos maliciosos seja usado para, por exemplo, enganar o código de chamada para salvar informações privadas (por exemplo, uma chave secreta usada pelo aplicativo) em um campo de banco de dados não relacionado, de onde poderia ser exposto (consulte http://chat.stackoverflow.com/transcript/message/35999686#35999686 ).

Exemplo de uso:

>>> 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
fonte
1
Obrigado, Zero! Bom exemplo.
Ethan Furman
Se você tiver seu código em um módulo (enumencoder.py, por exemplo), deverá importar a classe que você analisa de JSON para dict. Por exemplo, neste caso, você deve importar a classe Status no módulo enumencoder.py.
Francisco Manuel Garca Botella
Minha preocupação não era com o código de chamada malicioso, mas com solicitações maliciosas para um servidor da web. Como você mencionou, os dados privados podem ser expostos em uma resposta ou podem ser usados ​​para manipular o fluxo do código. Obrigado por atualizar sua resposta. Seria ainda melhor se o exemplo de código principal fosse seguro.
Jared Deckard,
1
@JaredDeckard minhas desculpas, você estava certo e eu errado. Eu atualizei a resposta de acordo. Obrigado pela sua contribuição! Isso tem sido educativo (e disciplinador).
Zero Piraeus
esta opção seria mais apropriada if isinstance(obj, Enum):?
user7440787
126

Eu sei que isso é antigo, mas acho que isso vai ajudar as pessoas. Acabei de passar por esse problema exato e descobri se você está usando enums de string, declarar seus enums como uma subclasse de strfunciona bem para quase todas as situações:

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

Irá produzir:

LogLevel.DEBUG
"DEBUG"
DEBUG
LogLevel.DEBUG

Como você pode ver, carregar o JSON gera a string, DEBUGmas é facilmente convertível de volta em um objeto LogLevel. Uma boa opção se você não deseja criar um JSONEncoder personalizado.

Justin Carter
fonte
1
Obrigado. Embora eu seja principalmente contra as heranças múltiplas, isso é muito legal e é assim que estou indo. Nenhum codificador extra necessário :)
Vinicius Dantas
@madjardi, você pode explicar o problema que está tendo? Nunca tive um problema com o valor da string ser diferente do nome do atributo no enum. Estou entendendo mal o seu comentário?
Justin Carter
1
class LogLevel(str, Enum): DEBUG = 'Дебаг' INFO = 'Инфо'neste caso, enum with strnão funciona corretamente (
madjardi
1
Você também pode fazer esse truque com outros tipos de base, por exemplo (não sei como formatar isso nos comentários, mas a essência é clara: "class Shapes (int, Enum): square = 1 circle = 2" funciona ótimo sem necessidade de um codificador. Obrigado, esta é uma ótima abordagem!
NoCake,
Funciona como um encanto, obrigado! Deve ser aceito como resposta.
Realfun
72

A resposta correta depende do que você pretende fazer com a versão serializada.

Se você vai desserializar de volta para Python, veja a resposta de Zero .

Se sua versão serializada for para outro idioma, você provavelmente deseja usar um IntEnum, que é serializado automaticamente como o número inteiro correspondente:

from enum import IntEnum
import json

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

json.dumps(Status.success)

e isso retorna:

'0'
Ethan Furman
fonte
5
@AShelly: A pergunta foi marcada com Python3.4, e esta resposta é 3.4+ específica.
Ethan Furman
2
Perfeito. Se Enum for uma string, você usaria em EnumMetavez deIntEnum
bholagabbar
5
@bholagabbar: Não, você usaria Enum, possivelmente com um strmixin -class MyStrEnum(str, Enum): ...
Ethan Furman
3
@bholagabbar, interessante. Você deve postar sua solução como uma resposta.
Ethan Furman,
1
Eu evitaria herdar diretamente de EnumMeta, que era apenas uma metaclasse. Em vez disso, observe que a implementação de IntEnum é de uma linha e você pode conseguir o mesmo strcom class StrEnum(str, Enum): ....
yungchin
17

No Python 3.7, pode apenas usar json.dumps(enum_obj, default=str)

kai
fonte
Parece bom, mas escreverá o nameof enum na string json. A melhor maneira será usar valueo enum.
eNca
O valor Enum pode ser usado porjson.dumps(enum_obj, default=lambda x: x.value)
eNca
10

Gostei da resposta de Zero Piraeus, mas modifiquei um pouco para trabalhar com a API para Amazon Web Services (AWS) conhecida como Boto.

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

Em seguida, adicionei este método ao meu modelo de dados:

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

Espero que isso ajude alguém.

Pretzel
fonte
Por que você precisa adicionar ToJsonao seu modelo de dados?
Yu Chen
2

Se você estiver usando jsonpicklea maneira mais fácil, veja a seguir.

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)

Após a serialização Json, você terá como esperado em {"status": 0}vez de

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

Isso funcionou para mim:

class Status(Enum):
    success = 0

    def __json__(self):
        return self.value

Não precisava mudar mais nada. Obviamente, você só obterá o valor disso e precisará fazer algum outro trabalho se quiser converter o valor serializado de volta para o enum posteriormente.

DukeSilver
fonte
2
Não vejo nada nos documentos descrevendo esse método mágico. Você está usando alguma outra biblioteca JSON ou tem uma customizada em JSONEncoderalgum lugar?
0x5453