Serialising an Enum member to JSON

66,920

Solution 1

If you want to encode an arbitrary enum.Enum member to JSON and then decode it as the same enum member (rather than simply the enum member's value attribute), you can do so by writing a custom JSONEncoder class, and a decoding function to pass as the object_hook argument to json.load() or 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_enum function relies on the JSON having been encoded using EnumEncoder, or something which behaves identically to it.

The restriction to members of PUBLIC_ENUMS is necessary to avoid a maliciously crafted text being used to, for example, trick calling code into saving private information (e.g. a secret key used by the application) to an unrelated database field, from where it could then be exposed (see https://chat.stackoverflow.com/transcript/message/35999686#35999686).

Example usage:

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

Solution 2

I know this is old but I feel this will help people. I just went through this exact problem and discovered if you're using string enums, declaring your enums as a subclass of str works well for almost all situations:

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

Will output:

LogLevel.DEBUG
"DEBUG"
DEBUG
LogLevel.DEBUG

As you can see, loading the JSON outputs the string DEBUG but it is easily castable back into a LogLevel object. A good option if you don't want to create a custom JSONEncoder.

Solution 3

The correct answer depends on what you intend to do with the serialized version.

If you are going to unserialize back into Python, see Zero's answer.

If your serialized version is going to another language then you probably want to use an IntEnum instead, which is automatically serialized as the corresponding integer:

from enum import IntEnum
import json

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

json.dumps(Status.success)

and this returns:

'0'

Solution 4

In Python >= 3.7, can just use json.dumps(enum_obj, default=str)

If you want to use the enum value, you can do

json.dumps(enum_obj, default=lambda x: x.value)

or if you want to use the enum name,

json.dumps(enum_obj, default=lambda x: x.name)

Solution 5

You just need to inherit from str or int class:

from enum import Enum, unique

@unique            
class StatusEnum(int, Enum):
    pending: int = 11                                      
    approved: int = 15                                       
    declined: int = 266

That's it, it will be serialised using any JSON encoder.

Share:
66,920
Bilal Syed Hussain
Author by

Bilal Syed Hussain

Updated on December 04, 2021

Comments

  • Bilal Syed Hussain
    Bilal Syed Hussain over 2 years

    How do I serialise a Python Enum member to JSON, so that I can deserialise the resulting JSON back into a Python object?

    For example, this code:

    from enum import Enum    
    import json
    
    class Status(Enum):
        success = 0
    
    json.dumps(Status.success)
    

    results in the error:

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

    How can I avoid that?

  • Bilal Syed Hussain
    Bilal Syed Hussain almost 10 years
    I like to unserialize back into Python
  • Ethan Furman
    Ethan Furman almost 10 years
    Thanks, Zero! Nice example.
  • Sunilsingh
    Sunilsingh over 9 years
    In python3.2, This doesn't actually return what the answer says. I get: >>> json.dumps(Status.success) -> 'Status.success'
  • Ethan Furman
    Ethan Furman over 9 years
    @AShelly: The question was tagged with Python3.4, and this answer is 3.4+ specific.
  • Francisco Manuel Garca Botella
    Francisco Manuel Garca Botella almost 8 years
    If you have your code in a module(enumencoder.py, for example), you must import the class that you parse from JSON to dict. For example, in this case, you must import the class Status in the module enumencoder.py.
  • Jared Deckard
    Jared Deckard about 7 years
    My concern was not about malicious calling code, but malicious requests to a web server. As you mentioned, the private data could be exposed in a response, or it could be used to manipulate code flow. Thank you for updating your answer. It would be even better if the main code example was secure though.
  • Zero Piraeus
    Zero Piraeus about 7 years
    @JaredDeckard my apologies, you were right, and I was wrong. I've updated the answer accordingly. Thanks for your input! This has been educational (and chastening).
  • gabbar0x
    gabbar0x about 6 years
    Perfect. If you Enum is a string, you would use EnumMeta instead of IntEnum
  • Ethan Furman
    Ethan Furman about 6 years
    @bholagabbar: No, you would use Enum, possibly with a str mixin -- class MyStrEnum(str, Enum): ...
  • gabbar0x
    gabbar0x about 6 years
    I used EnumMeta and the json serialises and deserialises perfectly.
  • Ethan Furman
    Ethan Furman about 6 years
    @bholagabbar, interesting. You should post your solution as an answer.
  • yungchin
    yungchin almost 5 years
    I would avoid inheriting directly from EnumMeta, which was intended as a metaclass only. Instead, note that the implementation of IntEnum is a one-liner and you can achieve the same for str with class StrEnum(str, Enum): ....
  • Vinicius Dantas
    Vinicius Dantas over 4 years
    Thanks. Even though I am mostly against multiple inheritances, that's pretty neat and that's the way I am going with. No extra encoder needed :)
  • Justin Carter
    Justin Carter over 4 years
    @madjardi, can you elaborate on the problem you are having? I have never had a problem with the value of the string being different than the name of the attribute in the enum. Am I misunderstanding your comment?
  • madjardi
    madjardi over 4 years
    class LogLevel(str, Enum): DEBUG = 'Дебаг' INFO = 'Инфо' in this case enum with str no work properly (
  • Asotos
    Asotos about 4 years
    Wow! That and the str mixin cover most cases I'll ever need!
  • user7440787
    user7440787 about 4 years
    would this option be more appropriate if isinstance(obj, Enum): ?
  • ed22
    ed22 almost 4 years
    Is there a way to somehow 'annotate' the enum with the encoder class so that the encoder is used by default?
  • 0x5453
    0x5453 almost 4 years
    I don't see anything in the docs describing that magic method. Are you using some other JSON library, or do you have a custom JSONEncoder somewhere?
  • NoCake
    NoCake over 3 years
    You can also do this trick with other base types, for example (I don't know how to format this in the comments, but the gist is clear: "class Shapes(int, Enum): square=1 circle=2" works great w/o need for an encoder. Thanks, this is a great approach!
  • eNca
    eNca over 3 years
    Looks nice but it will write the name of enum into the json string. The better way will be to use value of the enum.
  • eNca
    eNca over 3 years
    Enum value can be used by json.dumps(enum_obj, default=lambda x: x.value)
  • Yu Chen
    Yu Chen over 3 years
    Why do you need to add ToJson to your data model?
  • Realfun
    Realfun over 3 years
    Works like a charm, thanks! Should be accepted as answer instead.
  • Dequn
    Dequn over 3 years
    Great solution, how do you find this.
  • koks der drache
    koks der drache over 3 years
    This str mixin can have unintended side effects: See this question.
  • FSCKur
    FSCKur about 3 years
    Possibly this user is importing simplejson?
  • wr200m
    wr200m about 3 years
    However, this would not work if an Enum is used as a key. It would still complain of "TypeError: keys must be str, int, float, bool or None" - Most of the solutions above do not take care of the usage as key. Enum is supposed to be hashable.
  • Charles Naccio
    Charles Naccio almost 3 years
    Such an elegant solution! Thanks brother!
  • rjh
    rjh over 2 years
    Note that this serializes to the enum value, in string form, e.g. "11" or "15".