การต่อสมาชิก Enum เป็น JSON


96

ฉันจะทำให้EnumสมาชิกPython เป็นอนุกรมเป็นJSON ได้อย่างไรเพื่อที่ฉันจะสามารถยกเลิกการกำหนดค่า JSON ที่เป็นผลลัพธ์กลับไปยังวัตถุ Python ได้

ตัวอย่างเช่นรหัสนี้:

from enum import Enum    
import json

class Status(Enum):
    success = 0

json.dumps(Status.success)

ผลลัพธ์ในข้อผิดพลาด:

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

ฉันจะหลีกเลี่ยงสิ่งนั้นได้อย่างไร?

คำตอบ:


52

หากคุณต้องการเข้ารหัสenum.Enumสมาชิกโดยพลการเป็น JSON แล้วถอดรหัสเป็นสมาชิก enum เดียวกัน (แทนที่จะเป็นเพียงvalueแอตทริบิวต์ของสมาชิก enum ) คุณสามารถทำได้โดยการเขียนJSONEncoderคลาสที่กำหนดเองและฟังก์ชันการถอดรหัสเพื่อส่งผ่านเป็นobject_hookอาร์กิวเมนต์ไปยังjson.load()หรือ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_enumฟังก์ชั่นอาศัยอยู่กับ JSON ได้รับการเข้ารหัสโดยใช้EnumEncoderหรือสิ่งที่มีลักษณะการทำงานเหมือนกันกับมัน

ข้อ จำกัด สำหรับสมาชิกPUBLIC_ENUMSเป็นสิ่งจำเป็นเพื่อหลีกเลี่ยงข้อความที่ออกแบบมาเพื่อประสงค์ร้ายที่ใช้เพื่อหลอกล่อรหัสการโทรให้บันทึกข้อมูลส่วนตัว (เช่นรหัสลับที่แอปพลิเคชันใช้) ไปยังฟิลด์ฐานข้อมูลที่ไม่เกี่ยวข้องซึ่งอาจถูกเปิดเผยได้ (ดูhttp://chat.stackoverflow.com/transcript/message/35999686#35999686 )

ตัวอย่างการใช้งาน:

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

1
ขอบคุณ Zero! ตัวอย่างที่ดี
Ethan Furman

หากคุณมีโค้ดของคุณในโมดูล (เช่น enumencoder.py เป็นต้น) คุณต้องนำเข้าคลาสที่คุณแยกวิเคราะห์จาก JSON เป็น dict ตัวอย่างเช่นในกรณีนี้คุณต้องนำเข้าสถานะคลาสในโมดูล enumencoder.py
Francisco Manuel Garca Botella

ความกังวลของฉันไม่ได้เกี่ยวกับรหัสการโทรที่เป็นอันตราย แต่เป็นการร้องขอที่เป็นอันตรายไปยังเว็บเซิร์ฟเวอร์ ดังที่คุณกล่าวไว้ข้อมูลส่วนตัวอาจถูกเปิดเผยในการตอบสนองหรืออาจใช้เพื่อจัดการกับการไหลของโค้ด ขอบคุณสำหรับการอัปเดตคำตอบของคุณ จะดีกว่านี้ถ้าตัวอย่างโค้ดหลักมีความปลอดภัย
Jared Deckard

1
@JaredDeckard ขอโทษของฉันคุณพูดถูกและฉันผิด ฉันได้อัปเดตคำตอบตามนั้น ขอบคุณสำหรับข้อมูลของคุณ! สิ่งนี้ได้รับการศึกษา (และการตีสอน)
Zero Piraeus

ตัวเลือกนี้จะเหมาะสมกว่าif isinstance(obj, Enum):ไหม
user7440787

114

ฉันรู้ว่ามันเก่า แต่ฉันรู้สึกว่านี่จะช่วยคนได้ ฉันเพิ่งผ่านปัญหาที่แน่นอนนี้และพบว่าคุณใช้ string enums หรือไม่โดยประกาศ enums ของคุณเป็นคลาสย่อยที่strทำงานได้ดีในเกือบทุกสถานการณ์:

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

จะส่งออก:

LogLevel.DEBUG
"DEBUG"
DEBUG
LogLevel.DEBUG

อย่างที่คุณเห็นการโหลด JSON จะส่งออกสตริงDEBUGแต่สามารถส่งกลับไปยังวัตถุ LogLevel ได้อย่างง่ายดาย ตัวเลือกที่ดีหากคุณไม่ต้องการสร้าง JSONEncoder ที่กำหนดเอง


1
ขอบคุณ. แม้ว่าส่วนใหญ่ฉันจะต่อต้านการสืบทอดหลายอย่าง แต่ก็ค่อนข้างเรียบร้อยและนั่นคือวิธีที่ฉันจะทำ ไม่จำเป็นต้องใช้ตัวเข้ารหัสเพิ่มเติม :)
Vinicius Dantas

@madjardi คุณสามารถอธิบายปัญหาที่คุณประสบอย่างละเอียดได้หรือไม่? ฉันไม่เคยมีปัญหากับค่าของสตริงที่แตกต่างจากชื่อของแอตทริบิวต์ใน enum ฉันเข้าใจความคิดเห็นของคุณผิดหรือเปล่า?
Justin Carter

1
class LogLevel(str, Enum): DEBUG = 'Дебаг' INFO = 'Инфо'ในกรณีนี้enum with strทำงานไม่ถูกต้อง (
madjardi

1
คุณสามารถใช้เคล็ดลับนี้กับประเภทฐานอื่น ๆ ได้เช่น (ฉันไม่รู้ว่าจะจัดรูปแบบสิ่งนี้ในความคิดเห็นอย่างไร แต่ส่วนสำคัญชัดเจน: "class Shapes (int, Enum): square = 1 circle = 2" works ดีมากโดยไม่ต้องใช้ตัวเข้ารหัสขอบคุณนี่เป็นแนวทางที่ยอดเยี่ยม!
NoCake

71

คำตอบที่ถูกต้องขึ้นอยู่กับสิ่งที่คุณตั้งใจจะทำกับเวอร์ชันซีเรียลไลซ์

หากคุณกำลังจะกลับเข้าสู่ unserialize หลามดูคำตอบของศูนย์

หากเวอร์ชันซีเรียลไลซ์ของคุณเป็นภาษาอื่นคุณอาจต้องการใช้IntEnumแทนซึ่งจะถูกทำให้เป็นอนุกรมโดยอัตโนมัติเป็นจำนวนเต็มที่เกี่ยวข้อง:

from enum import IntEnum
import json

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

json.dumps(Status.success)

และผลตอบแทนนี้:

'0'

5
@AShelly: คำถามถูกแท็กPython3.4และคำตอบนี้มีความเฉพาะเจาะจง 3.4+
Ethan Furman

2
สมบูรณ์แบบ. หากคุณ Enum เป็นสตริงคุณจะใช้EnumMetaแทนIntEnum
bholagabbar

5
@bholagabbar: ไม่คุณอาจจะใช้Enumกับstrmixin -class MyStrEnum(str, Enum): ...
Ethan Furman

3
@bholagabbar น่าสนใจ. คุณควรโพสต์วิธีแก้ปัญหาของคุณเป็นคำตอบ
Ethan Furman

1
ฉันจะหลีกเลี่ยงการสืบทอดโดยตรงจากEnumMetaซึ่งมีไว้เพื่อเป็น metaclass เท่านั้น แต่ทราบว่าการดำเนินการIntEnum เป็นหนึ่งซับและคุณจะประสบความสำเร็จเหมือนกันสำหรับกับstr class StrEnum(str, Enum): ...
yungchin

15

ใน Python 3.7 สามารถใช้ไฟล์ json.dumps(enum_obj, default=str)


ดูดี แต่จะเขียนnameenum ลงในสตริง json วิธีที่ดีกว่าคือการใช้valueenum
eNca

ค่า Enum สามารถใช้ได้ภายในjson.dumps(enum_obj, default=lambda x: x.value)
eNca

10

ฉันชอบคำตอบของ Zero Piraeus แต่แก้ไขเล็กน้อยเพื่อทำงานกับ API สำหรับ Amazon Web Services (AWS) ที่เรียกว่า Boto

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

จากนั้นฉันก็เพิ่มวิธีนี้ในโมเดลข้อมูลของฉัน:

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

ฉันหวังว่านี่จะช่วยใครบางคนได้


ทำไมคุณต้องเพิ่มToJsonในโมเดลข้อมูลของคุณ?
Yu Chen

2

หากคุณใช้jsonpickleวิธีที่ง่ายที่สุดควรดูด้านล่าง

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)

หลังจาก Json serialization คุณจะได้ตามที่คาดไว้{"status": 0}แทนที่จะเป็น

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

-1

สิ่งนี้ใช้ได้ผลสำหรับฉัน:

class Status(Enum):
    success = 0

    def __json__(self):
        return self.value

ไม่ต้องเปลี่ยนอะไรอีก เห็นได้ชัดว่าคุณจะได้รับค่าจากสิ่งนี้เท่านั้นและจะต้องทำงานอื่น ๆ อีกหากคุณต้องการแปลงค่าซีเรียลไลซ์กลับเป็น enum ในภายหลัง


2
ฉันไม่เห็นอะไรเลยในเอกสารที่อธิบายถึงวิธีการวิเศษนั้น คุณใช้ไลบรารี JSON อื่น ๆ หรือคุณมีที่กำหนดเองJSONEncoderหรือไม่?
0x5453
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.