195

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?

Paul P
  • 3,346
  • 2
  • 12
  • 26
Bilal Syed Hussain
  • 8,664
  • 11
  • 38
  • 44

9 Answers9

279

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.

Justin Carter
  • 2,830
  • 2
  • 7
  • 6
  • 7
    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 :) – Vinicius Dantas Nov 02 '19 at 23:17
  • @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? – Justin Carter Nov 21 '19 at 12:59
  • 3
    `class LogLevel(str, Enum): DEBUG = 'Дебаг' INFO = 'Инфо'` in this case `enum with str` no work properly ( – madjardi Nov 22 '19 at 06:35
  • 1
    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! – NoCake Sep 23 '20 at 19:25
  • 2
    This str mixin can have unintended side effects: See [this question](https://stackoverflow.com/questions/65339635/how-to-deserialise-enumeration-with-string-representation). – koks der drache Jan 18 '21 at 07:07
  • Side effects fixed in JSONS 1.6.1 – Trenomarcus Jul 20 '23 at 13:13
114

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'
Community
  • 1
  • 1
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
73

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'}
Community
  • 1
  • 1
Zero Piraeus
  • 56,143
  • 27
  • 150
  • 160
  • 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. – Francisco Manuel Garca Botella May 12 '16 at 16:17
  • 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. – Jared Deckard Mar 07 '17 at 15:39
  • 1
    @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). – Zero Piraeus Mar 07 '17 at 18:50
  • would this option be more appropriate `if isinstance(obj, Enum):` ? – user7440787 Apr 16 '20 at 17:02
  • 1
    Is there a way to somehow 'annotate' the enum with the encoder class so that the encoder is used by default? – ed22 Jul 17 '20 at 02:46
  • values() does not exist and without it "unsupported operand type(s) for 'in': 'EnumType' and 'EnumType'" – John Glen Mar 05 '23 at 01:33
  • @JohnGlen `PUBLIC_ENUMS` must be a dictionary, as shown in the code in the answer. Your error message seems to indicate that you've accidentally made it an Enum instead. – Zero Piraeus Mar 05 '23 at 02:17
  • Ah, yes. I am thinking of making custom hooks so I can use the enums. – John Glen Mar 05 '23 at 15:42
41

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)

yiwei
  • 4,022
  • 9
  • 36
  • 54
kai
  • 1,640
  • 18
  • 11
  • 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 Oct 04 '20 at 07:43
  • 2
    Enum value can be used by `json.dumps(enum_obj, default=lambda x: x.value)` – eNca Oct 04 '20 at 07:49
  • 2
    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. – wr200m Apr 30 '21 at 23:02
31

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.

Farshid Ashouri
  • 16,143
  • 7
  • 52
  • 66
15

I liked Zero Piraeus' answer, but modified it slightly for working with the API for Amazon Web Services (AWS) known as Boto.

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

I then added this method to my data model:

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

I hope this helps someone.

Steen
  • 6,573
  • 3
  • 39
  • 56
Pretzel
  • 8,141
  • 16
  • 59
  • 84
5

If you are using jsonpickle the easiest way should look as below.

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)

After Json serialization you will have as expected {"status": 0} instead of

{"status": {"__objclass__": {"py/type": "__main__.Status"}, "_name_": "success", "_value_": 0}}
rafalkasa
  • 1,743
  • 20
  • 20
0

You can even combine the solutions mentioned above with the automatic value creation for Enums. I use this in combination with Pydantic and FastAPI to provide lower case names for a REST API:

from enum import Enum, auto
import json

class StrEnum(str, Enum):
    pass

# this creates nice lowercase and JSON serializable names
# https://docs.python.org/3/library/enum.html#using-automatic-values
class AutoNameLower(StrEnum):
    def _generate_next_value_(name, start, count, last_values):
        return name.lower()

class AutoNameLowerStrEnum(AutoNameLower):
    pass

class MyActualEnum(AutoNameLowerStrEnum):
    THIS = auto()
    THAT = auto()
    FOO = auto()
    BAR = auto()

print(MyActualEnum.THIS)
print(json.dumps(MyActualEnum.THIS))
print(list(MyActualEnum))

Console:

>>> MyActualEnum.THIS
>>> "this"
>>> [<MyActualEnum.THIS: 'this'>, <MyActualEnum.THAT: 'that'>, <MyActualEnum.FOO: 'foo'>, <MyActualEnum.BAR: 'bar'>]
Joe
  • 6,758
  • 2
  • 26
  • 47
-4

This worked for me:

class Status(Enum):
    success = 0

    def __json__(self):
        return self.value

Didn't have to change anything else. Obviously, you'll only get the value out of this and will need to do some other work if you want to convert the serialized value back into the enum later.

DukeSilver
  • 458
  • 1
  • 6
  • 22
  • 3
    I don't see anything in the [docs](https://docs.python.org/3/library/json.html) describing that magic method. Are you using some other JSON library, or do you have a custom `JSONEncoder` somewhere? – 0x5453 Aug 04 '20 at 20:04
  • Possibly this user is importing simplejson? – FSCKur Feb 17 '21 at 12:09