Menjalankan anggota Enum ke JSON

96

Bagaimana cara menserialisasi anggota Python Enumke JSON, sehingga saya dapat menghentikan JSON yang dihasilkan kembali menjadi objek Python?

Misalnya, kode ini:

from enum import Enum    
import json

class Status(Enum):
    success = 0

json.dumps(Status.success)

menghasilkan kesalahan:

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

Bagaimana saya bisa menghindarinya?

Bilal Syed Hussain
sumber

Jawaban:

52

Jika Anda ingin mengenkode anggota arbitrer enum.Enumke JSON dan kemudian mendekodekannya sebagai anggota enum yang sama (bukan hanya valueatribut anggota enum ), Anda dapat melakukannya dengan menulis JSONEncoderkelas khusus , dan fungsi dekode untuk diteruskan sebagai object_hookargumen ke json.load()atau 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

The as_enumfungsi bergantung pada JSON yang telah dikodekan menggunakan EnumEncoder, atau sesuatu yang berperilaku identik dengan itu.

Pembatasan untuk anggota PUBLIC_ENUMSdiperlukan untuk menghindari teks perusak yang digunakan untuk, misalnya, menipu kode pemanggil untuk menyimpan informasi pribadi (misalnya kunci rahasia yang digunakan oleh aplikasi) ke bidang database yang tidak terkait, dari mana kemudian dapat diekspos (lihat http://chat.stackoverflow.com/transcript/message/35999686#35999686 ).

Contoh penggunaan:

>>> 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'}
Nol Piraeus
sumber
1
Terima kasih, Zero! Contoh yang bagus.
Ethan Furman
Jika Anda memiliki kode Anda dalam modul (enumencoder.py, misalnya), Anda harus mengimpor kelas yang Anda parse dari JSON ke dict. Misalnya, dalam kasus ini, Anda harus mengimpor Status kelas di modul enumencoder.py.
Francisco Manuel Garca Botella
Perhatian saya bukan tentang kode panggilan berbahaya, tetapi permintaan jahat ke server web. Seperti yang Anda sebutkan, data pribadi dapat diekspos dalam sebuah respons, atau dapat digunakan untuk memanipulasi aliran kode. Terima kasih telah memperbarui jawaban Anda. Akan lebih baik lagi jika contoh kode utama aman.
Jared Deckard
1
@JaredDeckard permintaan maaf saya, Anda benar, dan saya salah. Saya telah memperbarui jawabannya. Terima kasih atas masukan Anda! Ini mendidik (dan menegur).
Nol Piraeus
apakah opsi ini lebih tepat if isinstance(obj, Enum):?
pengguna7440787
115

Saya tahu ini sudah tua tetapi saya merasa ini akan membantu orang. Saya baru saja membahas masalah ini dan menemukan jika Anda menggunakan enum string, menyatakan enum Anda sebagai subkelas strberfungsi dengan baik untuk hampir semua situasi:

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

Akan menghasilkan:

LogLevel.DEBUG
"DEBUG"
DEBUG
LogLevel.DEBUG

Seperti yang Anda lihat, memuat JSON mengeluarkan string DEBUGtetapi dengan mudah dapat diubah kembali ke objek LogLevel. Pilihan yang bagus jika Anda tidak ingin membuat JSONEncoder kustom.

Justin Carter
sumber
1
Terima kasih. Meskipun saya sebagian besar menentang banyak warisan, itu cukup rapi dan itulah yang akan saya lakukan. Tidak diperlukan encoder tambahan :)
Vinicius Dantas
@madjardi, bisa uraikan masalah yang anda hadapi? Saya tidak pernah memiliki masalah dengan nilai string yang berbeda dari nama atribut di enum. Apakah saya salah paham dengan komentar Anda?
Justin Carter
1
class LogLevel(str, Enum): DEBUG = 'Дебаг' INFO = 'Инфо'dalam hal ini enum with strtidak ada yang berfungsi dengan baik (
madjardi
1
Anda juga dapat melakukan trik ini dengan jenis dasar lainnya, misalnya (saya tidak tahu bagaimana memformatnya di komentar, tetapi intinya jelas: "class Shapes (int, Enum): square = 1 circle = 2" berfungsi besar tanpa perlu encoder. Terima kasih, ini adalah pendekatan yang bagus!
NoCake
71

Jawaban yang benar tergantung pada apa yang ingin Anda lakukan dengan versi serial.

Jika Anda akan membatalkan serialisasi kembali ke Python, lihat jawaban Zero .

Jika versi serial Anda menggunakan bahasa lain maka Anda mungkin ingin menggunakan sebagai IntEnumgantinya, yang secara otomatis diserialkan sebagai integer yang sesuai:

from enum import IntEnum
import json

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

json.dumps(Status.success)

dan ini mengembalikan:

'0'
Ethan Furman
sumber
5
@AShelly: Pertanyaannya telah diberi tag Python3.4, dan jawaban ini spesifik 3.4+.
Ethan Furman
2
Sempurna. Jika Anda Enum adalah string, Anda akan menggunakan EnumMetabukannyaIntEnum
bholagabbar
5
@bholagabbar: Tidak, Anda akan menggunakan Enum, mungkin dengan strmixin -class MyStrEnum(str, Enum): ...
Ethan Furman
3
@bholagabbar, menarik. Anda harus memposting solusi Anda sebagai jawaban.
Ethan Furman
1
Saya akan menghindari mewarisi langsung dari EnumMeta, yang dimaksudkan sebagai metaclass saja. Sebagai gantinya, perhatikan bahwa penerapannya IntEnum adalah satu baris dan Anda dapat mencapai hal yang sama strdengan class StrEnum(str, Enum): ....
yungchin
15

Di Python 3.7, cukup gunakan json.dumps(enum_obj, default=str)

kai
sumber
Kelihatannya bagus tapi itu akan menulis nameenum ke dalam string json. Cara yang lebih baik adalah dengan menggunakan valueenum.
eNca
Nilai enum dapat digunakan olehjson.dumps(enum_obj, default=lambda x: x.value)
eNca
10

Saya menyukai jawaban Zero Piraeus, tetapi sedikit memodifikasinya untuk bekerja dengan API untuk Amazon Web Services (AWS) yang dikenal sebagai Boto.

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

Saya kemudian menambahkan metode ini ke model data saya:

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

Saya harap ini membantu seseorang.

Pretzel
sumber
Mengapa Anda perlu menambahkan ToJsonmodel data Anda?
Yu Chen
2

Jika Anda menggunakan jsonpicklecara termudah harus melihat seperti di bawah ini.

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)

Setelah serialisasi Json Anda akan mendapatkan seperti yang diharapkan, {"status": 0}bukan

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

Ini berhasil untuk saya:

class Status(Enum):
    success = 0

    def __json__(self):
        return self.value

Tidak perlu mengubah apa pun. Jelas, Anda hanya akan mendapatkan nilai dari ini dan perlu melakukan beberapa pekerjaan lain jika Anda ingin mengubah nilai serial kembali ke enum nanti.

DukeSilver
sumber
2
Saya tidak melihat apa pun di dokumen yang menjelaskan metode ajaib itu. Apakah Anda menggunakan pustaka JSON lain, atau apakah Anda memiliki pustaka khusus JSONEncoder?
0x5453