konradhalas / dacite

Simple creation of data classes from dictionaries.
MIT License
1.72k stars 107 forks source link

Cannot use Union of Dataclasses for `from_dict(data_class=...)` #214

Closed bachorp closed 1 year ago

bachorp commented 1 year ago

A union of dataclasses cannot be used for the parameter data_class of from_dict. See also https://github.com/konradhalas/dacite/issues/138.

Minimal example:

from dataclasses import dataclass
from typing import Union

import dacite

@dataclass
class A:
    foo: str

@dataclass
class B:
    bar: str

dacite.from_dict(Union[A, B], {})

This fails with TypeError: typing.Union[__main__.A, __main__.B] is not a module, class, method, or function.


Tested with python 3.10.9, dacite 1.7.0.

mciszczon commented 1 year ago

Hi, thanks for opening the issue—though you did not follow the bug template. Thus, I have to explicitly ask you here:

In your provided reproduction scenario, which of the data classes should be instantiated then? A or B? I'm just failing to see the use case of that. Your example contains errors anyway, but let's assume something like this:

@dataclass
class A:
    foo: Optional[str]
    bar: str

@dataclass
class B:
    foo: str
    bar: Optional[str]

dacite.from_dict(Union[A, B], {"foo": "a", "bar": "b"})

How can I decide which of the classes to instantiate? The input data is valid for both of them.

Maybe instead of a minimum reproducible example you could describe your particular use case?

bachorp commented 1 year ago

How can I decide which of the classes to instantiate? The input data is valid for both of them.

The first to match unless strict_unions_match is set (as specified here).

Maybe [..] you could describe your particular use case?

I want to parse a Json object constituting an authentication request. Authentication can be done by either (1) an authentication token or (2) a username and password. The corresponding types would look like this.

@dataclass
class AuthByToken:
    token: str

@dataclass
class AuthByUsernamePassword:
    username: str
    password: str

AuthRequest = AuthByToken | AuthByUsernamePassword

Parsing a given request (say {"token": "foo"}) as follows is not possible.

# Variant 1
dacite.from_dict(AuthRequest, {"token": "foo"})  # Throws 'TypeError'

However, if AuthRequest is instead nested in another type, it works.

@dataclass
class AuthRequestWrapper:
    authRequest: AuthRequest

# Variant 2
dacite.from_dict(AuthRequestWrapper, {"authRequest": {"token": "foo"}})  # This works

Although AuthRequest itself is not a dataclass, I do not see why we could / should not allow it as a target type.

mciszczon commented 1 year ago

@bachorp Thanks for providing the example. And why couldn't you just use the AuthRequestWrapper approach, i.e. nesting it within a separate dataclass? The only argument I see is that it is slightly more elaborate, but that's still just a few keywords more over the union approach, and is a bit more explicit in my opinion.

I am just not convinced this is such a big improvement, when one can simply create a wrapper dataclass to store any union they want, and it's much more explicit that way.

mciszczon commented 1 year ago

@bachorp Closing the issue, thanks for reporting and hope you've resolved your case this way or another!