mashumaro icon indicating copy to clipboard operation
mashumaro copied to clipboard

Allow propagation of class based discriminator settings to subclasses

Open Sanavesa opened this issue 4 months ago • 3 comments

  • mashumaro version: 3.12
  • Python version: 3.11.7
  • Operating System: Debian 12 (Bookworm)

Description

Issue Summary: When serializing and deserializing a hierarchy of dataclasses involving abstract classes using mashumaro's to_dict() and from_dict() methods, attempting to deserialize a dictionary back to an object through an abstract parent class results in a TypeError due to the inability to instantiate the abstract class, even when the dictionary represents a concrete subclass.

Expected Behavior: Deserialization of a dictionary to an object should be successful through any class in the hierarchy (including abstract classes) as long as the dictionary represents a concrete subclass that implements all abstract methods. This is expected to work because of the specified discriminator field that indicates the concrete subclass type.

Actual Behavior: Deserializing a dictionary to an object through an abstract class using the from_dict() method throws a TypeError, indicating an attempt to instantiate an abstract class with unimplemented abstract methods.

What I Did

  1. Define a base class Superclass that inherits from DataClassDictMixin. This class includes a Config subclass with a Discriminator configured on field type and includes all subtypes.
  2. Define an abstract class AbstractSubclass inheriting from Superclass with an abstract method foo().
  3. Define a concrete subclass ConcreteSubclass inheriting from AbstractSubclass, implementing the foo() method and setting the discriminator field type to "concrete_subclass".
  4. Serialize an instance of ConcreteSubclass to a dictionary using to_dict().
  5. Attempt to deserialize the dictionary back to an object using from_dict() on all classes in the hierarchy.
from abc import ABC, abstractmethod
from dataclasses import dataclass
from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig
from mashumaro.types import Discriminator


@dataclass
class Superclass(DataClassDictMixin):
    def __post_serialize__(self, d: dict) -> dict:
        # Embed the discriminator into the serialized dictionary
        return {"type": self.type, **d}

    class Config(BaseConfig):
        discriminator = Discriminator(field="type", include_subtypes=True)


@dataclass
class AbstractSubclass(Superclass, ABC):
    weight: float

    @abstractmethod
    def foo(self) -> None:
        raise NotImplementedError

    # NOTE: If this was uncommented, the error disappears!
    # class Config(BaseConfig):
    #     discriminator = Discriminator(field="type", include_subtypes=True)


@dataclass
class ConcreteSubclass(AbstractSubclass):
    type = "concrete_subclass"
    height: float

    def foo(self) -> None:
        print("bar")


inst = ConcreteSubclass(weight=80.0, height=180.0)
inst_dict = inst.to_dict()

assert inst_dict == {"type": "concrete_subclass", "weight": 80.0, "height": 180.0}  # OK

assert inst == ConcreteSubclass.from_dict(inst_dict)  # OK

assert inst == Superclass.from_dict(inst_dict)  # OK

assert inst == AbstractSubclass.from_dict(
    inst_dict
)  # TypeError: Can't instantiate abstract class AbstractSubclass with abstract method foo

Error Message:

TypeError: Can't instantiate abstract class AbstractSubclass with abstract methods foo

Temporary Workaround: Adding the same Config subclass with a Discriminator configuration to the AbstractSubclass resolves the issue. This suggests a potential inconsistency in how discriminator configurations are inherited or recognized in the class hierarchy.

Sanavesa avatar Mar 03 '24 11:03 Sanavesa