Nối tiếp một thành viên Enum cho JSON


96

Làm cách nào để tuần tự hóa một Enumthành viên Python thành JSON, để tôi có thể giải mã JSON kết quả trở lại thành một đối tượng Python?

Ví dụ, mã này:

from enum import Enum    
import json

class Status(Enum):
    success = 0

json.dumps(Status.success)

dẫn đến lỗi:

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

Làm thế nào tôi có thể tránh điều đó?

Câu trả lời:


52

Nếu bạn muốn mã hóa một enum.Enumthành viên tùy ý thành JSON và sau đó giải mã nó thành cùng một thành viên enum (thay vì chỉ đơn giản là valuethuộc tính của thành viên enum ), bạn có thể làm như vậy bằng cách viết một JSONEncoderlớp tùy chỉnh và một hàm giải mã để chuyển làm object_hookđối số cho json.load()hoặc 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

Các as_enumchức năng dựa vào JSON đã được mã hóa sử dụng EnumEncoder, hoặc một cái gì đó mà cư xử hệt với nó.

Hạn chế đối với các thành viên của PUBLIC_ENUMSlà cần thiết để tránh một văn bản được tạo thủ công độc hại được sử dụng, chẳng hạn như lừa mã gọi để lưu thông tin cá nhân (ví dụ: khóa bí mật được ứng dụng sử dụng) vào trường cơ sở dữ liệu không liên quan, từ đó nó có thể bị lộ (xem http://chat.stackoverflow.com/transcript/message/35999686#35999686 ).

Ví dụ sử dụng:

>>> 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
Cảm ơn, Zero! Một ví dụ đẹp.
Ethan Furman

Nếu bạn có mã của mình trong một mô-đun (ví dụ: enumencoder.py), bạn phải nhập lớp mà bạn phân tích cú pháp từ JSON thành dict. Ví dụ, trong trường hợp này, bạn phải nhập Trạng thái lớp trong mô-đun enumencoder.py.
Francisco Manuel Garca Botella

Mối quan tâm của tôi không phải về mã gọi độc hại, mà là các yêu cầu độc hại đến máy chủ web. Như bạn đã đề cập, dữ liệu cá nhân có thể được tiết lộ trong một phản hồi hoặc nó có thể được sử dụng để thao túng dòng mã. Cảm ơn bạn đã cập nhật câu trả lời của bạn. Sẽ tốt hơn nếu ví dụ mã chính được bảo mật.
Jared Deckard

1
@JaredDeckard xin lỗi của tôi, bạn đã đúng, và tôi đã sai. Tôi đã cập nhật câu trả lời cho phù hợp. Cảm ơn vì đầu vào của bạn! Điều này đã được giáo dục (và trong sạch).
Zero Piraeus

tùy chọn này sẽ thích hợp hơn if isinstance(obj, Enum):?
user7440787

117

Tôi biết điều này đã cũ nhưng tôi cảm thấy điều này sẽ giúp ích cho mọi người. Tôi vừa mới xem xét vấn đề chính xác này và phát hiện ra nếu bạn đang sử dụng chuỗi enums, việc khai báo enums của bạn như một lớp con strhoạt động tốt cho hầu hết các trường hợp:

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

Sẽ xuất:

LogLevel.DEBUG
"DEBUG"
DEBUG
LogLevel.DEBUG

Như bạn có thể thấy, việc tải JSON sẽ xuất ra chuỗi DEBUGnhưng nó có thể dễ dàng chuyển trở lại thành một đối tượng LogLevel. Một tùy chọn tốt nếu bạn không muốn tạo bộ mã hóa JSONEnc tùy chỉnh.


1
Cảm ơn. Mặc dù tôi chủ yếu chống lại việc thừa kế nhiều thứ, nhưng điều đó khá gọn gàng và đó là cách tôi đang làm. Không cần thêm bộ mã hóa :)
Vinicius Dantas

@madjardi, bạn có thể nói rõ hơn về vấn đề bạn đang gặp phải không? Tôi chưa bao giờ gặp sự cố với giá trị của chuỗi khác với tên của thuộc tính trong enum. Tôi đang hiểu sai bình luận của bạn?
Justin Carter

1
class LogLevel(str, Enum): DEBUG = 'Дебаг' INFO = 'Инфо'trong trường hợp này enum with strkhông hoạt động bình thường (
madjardi

1
Bạn cũng có thể thực hiện thủ thuật này với các kiểu cơ sở khác, chẳng hạn (tôi không biết cách định dạng này trong nhận xét, nhưng ý chính rất rõ ràng: "class Shapes (int, Enum): square = 1 circle = 2" hoạt động rất cần một bộ mã hóa. Cảm ơn, đây là một cách tiếp cận tuyệt vời!
NoCake

71

Câu trả lời chính xác phụ thuộc vào những gì bạn định làm với phiên bản được đăng nhiều kỳ.

Nếu bạn định phi công nghệ hóa trở lại Python, hãy xem câu trả lời của Zero .

Nếu phiên bản được tuần tự hóa của bạn chuyển sang một ngôn ngữ khác thì bạn có thể muốn sử dụng một ngôn ngữ IntEnumthay thế, được tự động tuần tự hóa dưới dạng số nguyên tương ứng:

from enum import IntEnum
import json

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

json.dumps(Status.success)

và điều này trả về:

'0'

5
@AShelly: Câu hỏi đã được gắn thẻ Python3.4và câu trả lời này là 3,4+ cụ thể.
Ethan Furman

2
Hoàn hảo. Nếu bạn Enum là một chuỗi, bạn sẽ sử dụng EnumMetathay vìIntEnum
bholagabbar

5
@bholagabbar: Không, bạn sẽ sử dụng Enum, có thể với một strmixin -class MyStrEnum(str, Enum): ...
Ethan Furman

3
@bholagabbar, thú vị. Bạn nên đăng giải pháp của mình như một câu trả lời.
Ethan Furman

1
Tôi sẽ tránh kế thừa trực tiếp từ EnumMeta, vốn chỉ được dùng như một siêu kính. Thay vào đó, hãy lưu ý rằng việc triển khai IntEnum là một lớp lót và bạn có thể đạt được điều tương tự đối strvới với class StrEnum(str, Enum): ....
yungchin

15

Trong Python 3.7, có thể sử dụng json.dumps(enum_obj, default=str)


Trông đẹp nhưng nó sẽ viết nameenum vào chuỗi json. Cách tốt hơn sẽ là sử dụng valueenum.
eNca

Giá trị json.dumps(enum_obj, default=lambda x: x.value)
enum

10

Tôi thích câu trả lời của Zero Piraeus, nhưng đã sửa đổi nó một chút để làm việc với API cho Amazon Web Services (AWS) được gọi là Boto.

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

Sau đó, tôi đã thêm phương pháp này vào mô hình dữ liệu của mình:

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

Tôi hi vọng điêu nay se giup được ai đo.


Tại sao bạn cần thêm ToJsonvào mô hình dữ liệu của mình?
Yu Chen

2

Nếu bạn đang sử dụng cách jsonpickledễ nhất sẽ như bên dưới.

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)

Sau khi tuần tự hóa Json, bạn sẽ có như mong đợi {"status": 0}thay vì

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

-1

Điều này đã làm việc cho tôi:

class Status(Enum):
    success = 0

    def __json__(self):
        return self.value

Không phải thay đổi bất cứ điều gì khác. Rõ ràng, bạn sẽ chỉ nhận được giá trị từ việc này và sẽ cần thực hiện một số công việc khác nếu bạn muốn chuyển đổi giá trị được tuần tự hóa trở lại vào enum sau này.


2
Tôi không thấy bất cứ điều gì trong các tài liệu mô tả phương pháp ma thuật đó. Bạn đang sử dụng một số thư viện JSON khác hay bạn có một tùy chỉnh JSONEncoderở đâu đó?
0x5453
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.