29

I have a dataclass with a field template of type Enum. When using the asdict function it converts my dataclass to a dictionary. Is it possible to use the value attribute of FoobarEnum to return the string value instead of the Enum object?

My initial idea was to use the dict_factory=dict parameter of the asdict function and provide my own factory but I couldn't figure out how to do this.

from dataclasses import dataclass, asdict
from enum import Enum


@dataclass
class Foobar:
  name: str
  template: "FoobarEnum"


class FoobarEnum(Enum):
  FIRST = "foobar"
  SECOND = "baz"


foobar = Foobar(name="John", template=FoobarEnum.FIRST)

print(asdict(foobar))

Current output:

{'name': 'John', 'template': <FoobarEnum.FIRST: 'foobar'>}

Goal:

{'name': 'John', 'template': 'foobar'}
bad_coder
  • 11,289
  • 20
  • 44
  • 72
Nepo Znat
  • 3,000
  • 5
  • 28
  • 49

7 Answers7

23

Actually you can do it. asdict has keyword argument dict_factory which allows you to handle your data there:

from dataclasses import dataclass, asdict
from enum import Enum


@dataclass
class Foobar:
  name: str
  template: "FoobarEnum"


class FoobarEnum(Enum):
  FIRST = "foobar"
  SECOND = "baz"


def custom_asdict_factory(data):

    def convert_value(obj):
        if isinstance(obj, Enum):
            return obj.value
        return obj

    return dict((k, convert_value(v)) for k, v in data)


foobar = Foobar(name="John", template=FoobarEnum.FIRST)

print(asdict(foobar, dict_factory=custom_asdict_factory))
# {'name': 'John', 'template': 'foobar'}
doomatel
  • 607
  • 1
  • 6
  • 8
  • 1
    While correct, this is such a counter productive way of handling it. The dataclass should control its asdict() output, not the user. – Joao Carlos Nov 29 '21 at 17:00
10
from dataclasses import dataclass, asdict
from enum import Enum


class FoobarEnum(Enum):
    FIRST = "foobar"
    SECOND = "baz"


@dataclass
class Foobar:
    name: str
    template: FoobarEnum


def my_dict(data):

    return {
        field: value.value if isinstance(value, Enum) else value
        for field, value in data
    }


foobar = Foobar(name="John", template=FoobarEnum.FIRST)

data = {'name': 'John', 'template': 'foobar'}

assert asdict(foobar, dict_factory=my_dict) == data
Evgeniy_Burdin
  • 627
  • 5
  • 14
  • Sorry. Missed it. I was in a rush writing the comments. I wanted to write it yesterday but did it only today, so technically you was the first. My apologies. – doomatel Nov 05 '20 at 11:27
8

I had a similar issue where I needed to serialize my dataclass object to JSON and solved it by adding str as the first class FoobarEnum inherits from:

import json
from dataclasses import dataclass, asdict
from enum import Enum


@dataclass
class Foobar:
  name: str
  template: "FoobarEnum"


class FoobarEnum(str, Enum):
  FIRST = "foobar"
  SECOND = "baz"


foobar = Foobar(name="John", template=FoobarEnum.FIRST)

print(json.dumps(asdict(foobar)))

It doesn't change the behavior of asdict, but I can serialize the object now.

Reference: Serialising an Enum member to JSON

BtLutz
  • 116
  • 1
  • 4
7

This can't be done with standard library except maybe by some metaclass enum hack I'm not aware of. Enum.name and Enum.value are builtin and not supposed to be changed.

The approach of using the dataclass default_factory isn't going to work either. Because default_factory is called to produce default values for the dataclass members, not to customize access to members.

You can either have the Enum member or the Enum.value as a dataclass member, and that's what asdict() will return.

If you want to keep an Enum member -not just the Enum.value- as a dataclass member, and have a function converting it to dictionary that returns the Enum.value instead of the Enum member, the correct way to do it is implementing your own method to return the dataclass as a dictionary.

from dataclasses import dataclass
from enum import Enum


class FoobarEnum(Enum):
    FIRST = "foobar"
    SECOND = "baz"


@dataclass
class Foobar:
    name: str
    template: FoobarEnum

    def as_dict(self):
        return {
            'name': self.name,
            'template': self.template.value
        }


# Testing.
print(Foobar(name="John", template=FoobarEnum.FIRST).as_dict())
# {'name': 'John', 'template': 'foobar'}

bad_coder
  • 11,289
  • 20
  • 44
  • 72
  • 1
    This approach won't work if you have the instance of your Foobar dataclass as a nested instance of another dataclass. In this case you'll need to make conversation manually and call your custom `as_dict` method on all of your entities. You can use `dict_factory` keyword argument which does exactly what yo need - https://docs.python.org/3.8/library/dataclasses.html#dataclasses.asdict. With this approach you can call `asdict` on you top entity and it will convert it and all its nested entities nice and easy. See my answer below with the code. – doomatel Nov 05 '20 at 08:50
3

You can implement the __deepcopy__ method to achieve what you want:

class FoobarEnum(Enum):
  FIRST = "foobar"
  SECOND = "baz"

  def __deepcopy__(self, memo):
      return self.value

The asdict function handles dataclasses, tuples, lists and dicts. In case of any other type it calls:

copy.deepcopy(obj)

Overriding __deepcopy__ like this is probably not the best idea though.

  • It would work but it's not a good idea. You'll have a string instead of Enum in case you need to make an actually `deepcopy` somewhere in your business logic. `asdict` has a keyword argument `dict_factory` which is designed specifically for this https://docs.python.org/3.8/library/dataclasses.html#dataclasses.asdict. See my answer with the code. – doomatel Nov 05 '20 at 08:53
2

Add __post_init__ like below.

from dataclasses import dataclass, asdict
from enum import Enum
from typing import Union


@dataclass
class Foobar:
    name:str
    template: Union["FoobarEnum", str]
    def __post_init__(self):
        self.template = self.template.value

class FoobarEnum(Enum):
    FIRST = "foobar"
    SECOND = "baz"

foobar = Foobar(name="John", template=FoobarEnum.FIRST)
print(asdict(foobar))

Depending on the usecase, you can inherit from both str and Enum or have a init only field.

balki
  • 26,394
  • 30
  • 105
  • 151
  • This isn't a good idea. In case if you need an Enum you need an Enum and not string. You can do your Enum conversion in a custom `dict_factory` which is a keyword argument of `asdict` function - https://docs.python.org/3.8/library/dataclasses.html#dataclasses.asdict. See my answer with code example. – doomatel Nov 05 '20 at 08:59
-1

Have you tried this?

import json

def dumps(object):
    def default(o):
        if isinstance(o, Enum):
            # use enum value when JSON deserialize the enum
            return o.__dict__['_value_'] 
        else:
            return o.__dict__
    return json.dumps(object, default=default)

print(json.dumps(YOUR_OBJECT_CONTAINS_ENUMS, default=default))
RobotCharlie
  • 1,180
  • 15
  • 19
  • You can do exactly the same approach with `dict_factory` - a keyword argument of `asdict` https://docs.python.org/3.8/library/dataclasses.html#dataclasses.asdict. It does literally the same and you don't need to convert your dataclesses to JSON and back. Please see my answer with code example. – doomatel Nov 05 '20 at 09:01