24

My problem could be summarised by the following example:

from enum import Enum
import json

class FooBarType(Enum):
    standard = 0
    foo = 1
    bar = 2

dict = {'name': 'test', 'value': 'test', 'type': FooBarType.foo}

json.dumps(dict)

TypeError: <FooBarType.foo: 1> is not JSON serializable

I get a type error, because enums are not JSON serializable.

I primarily though of implementing a JsonEncoder and adding it to the json.dumps() call but I cannot change the line where json.dumps() call is made.

So, my question is : Is it possible to dump an enum in json without passing an encoder to json.dumps(), but instead, by adding class method(s) in FooBarType enum ?

I expect to extract the following json:

{'name': 'test', 'value': 'test', 'type': 'foo'}

or

{'name': 'test', 'value': 'test', 'type': 1}
Axel Borja
  • 3,718
  • 7
  • 36
  • 50
  • Even if you can't change the line where `json.dumps()` call is made you can monkeypatch the `dumps()` function in the `json` module after `import`ing it — which will then affect all uses of it from then on (in the script running). There's examples of patching the module in [this answer](http://stackoverflow.com/questions/18478287/making-object-json-serializable-with-regular-encoder/18561055#18561055), – martineau Apr 18 '16 at 16:48
  • 2
    What do you want to output? Is it the enum "name" FooBarType.foo or is it the actual value "1" ? – Nils Ziehn Apr 18 '16 at 16:50
  • @NilsZiehn I prefer to get the enum name, but if it is not possible the corresponding value will be sufficient. – Axel Borja Apr 18 '16 at 16:52

7 Answers7

46

Try:

from enum import Enum

# class StrEnum(str, Enum):
#     """Enum where members are also (and must be) strs"""

class Color(str, Enum):
    RED = 'red'
    GREEN = 'green'
    BLUE = 'blue'


data = [
    {
        'name': 'car',
        'color': Color.RED,
    },
    {
        'name': 'dog',
        'color': Color.BLUE,
    },
]

import json
print(json.dumps(data))

Result:

[
    {
        "name": "car",
        "color": "red"
    },
    {
        "name": "dog",
        "color": "blue"
    }
]
gil9red
  • 946
  • 11
  • 26
4

Sadly, there is no direct support for Enum in JSON.

The closest automatic support is to use IntEnum (which enum34 also supports), and then json will treat your enums as ints; of course, decoding them will give you an int back, but that is as good it gets without specifying your encoder/decoder.

Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
4

Just adding method(s) to the FooBarType enum won't do what you want.

As I mentioned in my comment, you can however use part of my answer to the question Making object JSON serializable with regular encoder to monkey-patch the json module so it will return the name (or value) of Enum members. I'm assuming you're using the enums34 module by Ethan Furman et al, which was backported to Python 2.7 since that version doesn't come with it built-in — it became part of the standard library in Python 3.4.

Note this will work even though you can't change the line where the json.dumps() call occurs as long as that happens after the patch is applied. This is because Python normally caches imported modules in sys.modules, i.e. they aren't reloaded everytime they are used in separate scripts — so any changes made this to them are "sticky" and remain in effect.

So for what you want to do, first create your own module to make the patch. For example: make_enum_json_serializable.py.

""" Module that monkey-patches the json module when it's imported so
JSONEncoder.default() automatically checks to see if the object being encoded
is an instance of an Enum type and, if so, returns its name.
"""
from enum import Enum
from json import JSONEncoder

_saved_default = JSONEncoder().default  # Save default method.

def _new_default(self, obj):
    if isinstance(obj, Enum):
        return obj.name  # Could also be obj.value
    else:
        return _saved_default

JSONEncoder.default = _new_default # Set new default method.

Then, in your own script, all you need to do is essentially add one line:

from enum import Enum
import json
import make_enum_json_serializable  # ADDED

class FooBarType(Enum):
    standard = 0
    foo = 1
    bar = 2

a_dict = {'name': 'spam', 'value': 42, 'type': FooBarType.foo}

print(json.dumps(a_dict))

Output:

{"type": "foo", "name": "spam", "value": 42}
martineau
  • 119,623
  • 25
  • 170
  • 301
  • Nice. Can you include the snippet for getting the enum back from the json? – Ethan Furman Feb 22 '17 at 16:31
  • @Ethan: As it stands, I don't think there's any way to do that because of the information loss—the fact that the value originated from an `Enum` isn't preserved. Off-hand I don't know how it could be since the JSON standard only allows values to be a string, number, object, array, **`true`**, **`false`**, or **`null`**. – martineau Feb 22 '17 at 18:42
  • @Ethan: The second part of [my answer](http://stackoverflow.com/a/18561055/355230) to the question [**_Making object JSON serializable with regular encoder_**](http://stackoverflow.com/questions/18478287/making-object-json-serializable-with-regular-encoder) describes a way to store pickle data as the JSON value, which means the actual instance value and its type could be reconstructed. Note that it does require providing a custom `object_hook=` function argument on any `json.loads()` calls. – martineau Feb 22 '17 at 19:02
  • Thanks. I was looking for an easy way to make a json file that could be used to transfer data to other systems as well as back to Python, but I'm not seeing it. – Ethan Furman Feb 22 '17 at 19:09
  • @Ethan: Unless those "other" systems have an enum-like type, I don't see doing that as possible. This answer will at least get the lower-level name or value transferred. – martineau Feb 22 '17 at 19:19
  • @Ethan: It would be easy to make this technique produce strings of the form `"EnumName(value)"` which would preserve most of the information. – martineau Feb 22 '17 at 23:28
  • 1
    If going that route I'd just use `"EnumName.Member"` to make the parsing easier. (And congrats on your badge. ;) – Ethan Furman Feb 22 '17 at 23:33
3

UPDATE: Please read the answer from @gil9red, I think it's better than mine!

I don't think there is a great way for this and you will lose features of the Enum.

Simplest option: Don't subclass Enum:

class FooBarType:
    standard = 0
    foo = 1
    bar = 2

dict = {'type': FooBarType.foo}
json.dumps(dict)

What you could also do:

class EnumIntValue(int):
    def __new__(cls, name, value):
        c = int.__new__(cls, int(value))
        c.name = name
        return c
    def __repr__(self):
        return self.name
    def __str__(self):
        return self.name

class FooBarType:
    standard = EnumIntValue('standard',0)
    foo = EnumIntValue('foo',0)
    bar = EnumIntValue('bar',2)

dict = {'type': FooBarType.foo}
json.dumps(dict)

This will actually give you

{"type": foo}

And therefore not really be valid json, but you can play around with it to fit your needs!

Nils Ziehn
  • 4,118
  • 6
  • 26
  • 40
1

I've recently bumped into a situation where I had to serialize an object that has a couple of Enum types as members.

Basically, I've just added a helper function that maps enum types to their name.

from enum import Enum, auto
from json import dumps

class Status(Enum):
    OK = auto()
    NOT_OK = auto()


class MyObject:
    def __init__(self, status):
        self.status = status


obja = MyObject(Status.OK)
objb = MyObject(Status.NOT_OK)

print(dumps(obja))
print(dumps(objb))

This of course fails with the error TypeError: Object of type MyObject is not JSON serializable, as the status member of the MyObject instances is not serializable.

from enum import Enum, auto
from json import dumps


def _prepare_for_serialization(obj):
    serialized_dict = dict()
    for k, v in obj.__dict__.items():
        serialized_dict[k] = v.name if isinstance(v, Enum) else v
    return serialized_dict


class Status(Enum):
    OK = auto()
    NOT_OK = auto()


class MyObject:
    def __init__(self, status):
        self.status = status


obja = MyObject(Status.OK)
objb = MyObject(Status.NOT_OK)

print(dumps(_prepare_for_serialization(obja)))
print(dumps(_prepare_for_serialization(objb)))

This prints:

{"status": "OK"}
{"status": "NOT_OK"}

Later on, I've used the same helper function to cherry-pick keys for the serialized dict.

TomerFi
  • 41
  • 1
  • 4
0

You can use a metaclass instead of an enum, and instead of multiple-inheritance without these side effects.

https://gist.github.com/earonesty/81e6c29fa4c54e9b67d9979ddbd8489d

For example:

class FooBarType(metaclass=TypedEnum):
    standard = 0
    foo = 1
    bar = 2

That way every instance is an integer and is also a FooBarType.

Metaclass below.

class TypedEnum(type):
    """This metaclass creates an enumeration that preserves isinstance(element, type)."""

    def __new__(mcs, cls, bases, classdict):
        """Discover the enum members by removing all intrinsics and specials."""
        object_attrs = set(dir(type(cls, (object,), {})))
        member_names = set(classdict.keys()) - object_attrs
        member_names = member_names - set(name for name in member_names if name.startswith("_") and name.endswith("_"))
        new_class = None
        base = None
        for attr in member_names:
            value = classdict[attr]
            if new_class is None:
                # base class for all members is the type of the value
                base = type(classdict[attr])
                ext_bases = (*bases, base)
                new_class = super().__new__(mcs, cls, ext_bases, classdict)
                setattr(new_class, "__member_names__", member_names)
            else:
                if not base == type(classdict[attr]):  # noqa
                    raise SyntaxError("Cannot mix types in TypedEnum")
            new_val = new_class.__new__(new_class, value)
            setattr(new_class, attr, new_val)

        for parent in bases:
            new_names = getattr(parent, "__member_names__", set())
            member_names |= new_names
            for attr in new_names:
                value = getattr(parent, attr)
                if not isinstance(value, base):
                    raise SyntaxError("Cannot mix inherited types in TypedEnum: %s from %s" % (attr, parent))
                # convert all inherited values to the new class
                setattr(new_class, attr, new_class(value))

        return new_class

    def __call__(cls, arg):
        for name in cls.__member_names__:
            if arg == getattr(cls, name):
                return type.__call__(cls, arg)
        raise ValueError("Invalid value '%s' for %s" % (arg, cls.__name__))

    @property
    def __members__(cls):
        """Sufficient to make the @unique decorator work."""

        class FakeEnum:  # pylint: disable=too-few-public-methods
            """Object that looks a bit like an Enum instance."""

            def __init__(self, name, value):
                self.name = name
                self.value = value

        return {name: FakeEnum(name, getattr(cls, name)) for name in cls.__member_names__}

    def __iter__(cls):
        """List all enum values."""
        return (getattr(cls, name) for name in cls.__member_names__)

    def __len__(cls):
        """Get number of enum values."""
        return len(cls.__member_names__)
Erik Aronesty
  • 11,620
  • 5
  • 64
  • 44
0

If you have a class model instead a dict, you can convert to json with this:

from enum import Enum
import json

class FooBarType(str, Enum):
    standard = 0
    foo = 1
    bar = 2

class ModelExample():
    def __init__(self, name: str, type: FooBarType) -> None:
        self.name = name
        self.type = type

# instantiate a class with your values
model_example = ModelExample(name= 'test', type= FooBarType.foo)

# vars -> get a dict of the class
json.loads(json.dumps(vars(model_example)))

Result:

{'name': 'test', 'type': '1'}
natielle
  • 380
  • 3
  • 14