dataclasses-json icon indicating copy to clipboard operation
dataclasses-json copied to clipboard

support typing.Literal

Open dustinlagoy opened this issue 2 years ago • 3 comments

First, dataclasses-json is great! Thanks for the hard work @lidatong and everyone else!

Currently dataclasses-json gives a warning if trying to call schema() with typing.Literal and no validation is performed. It would be nice for me if typing.Literal could be supported with marshmallow for validating that a field contains one of the items in the literal.

Here is a minimal example:

import dataclasses
import dataclasses_json
import marshmallow
import typing


@dataclasses.dataclass
class WithoutValidation(dataclasses_json.DataClassJsonMixin):
    choice: typing.Literal[1, 2, 3]


@dataclasses.dataclass
class WithValidation(dataclasses_json.DataClassJsonMixin):
    choice: typing.Literal[1, 2, 3] = dataclasses.field(
        metadata=dataclasses_json.config(
            mm_field=marshmallow.fields.Int(
                validate=marshmallow.validate.OneOf([1, 2, 3]))))


instance_a = WithoutValidation(4)

instance_b = WithoutValidation.schema().loads("""{"choice": "not-an-integer"}""")

instance_c = WithValidation.schema().loads("""{"choice": 4}""")

Here instance_a is generated without validation and works as expected (though mypy gives an expected error here as 4 is not one of the literal values).

instance_b is what "should" work if dataclasses_json supported typing.Literal. Here we should see an error as "not-an-integer" is not one of the literal options and is not even an integer at all. Instead of an error the load succeeds with the warning:

UserWarning: Unknown type typing.Literal[1, 2, 3] at WithoutValidation.choice: typing.Literal[1, 2, 3] It's advised to pass the correct marshmallow type to `mm_field`

instance_c shows the workaround mentioned in the warning and since 4 is not one of the valid choices this results in the expected error:

marshmallow.exceptions.ValidationError: {'choice': ['Must be one of: 1, 2, 3.']}

I realize it may not be straightforward to implement the generation of the Field from the literal, especially if the literal is composed of multiple types. Any thoughts? Is this something others would find useful? I would be happy to work on a PR if so.

dustinlagoy avatar Oct 07 '21 21:10 dustinlagoy

I personally would love that!

Molaire avatar Feb 02 '22 03:02 Molaire

This is not really an answer, but as an alternative if you don't need to work with marshmallow or dump dataclass to JSON schema for example, there is another library dataclass-wizard I developed that should work for this purpose. The error message is a bit different, but it does seem to get the job done when it comes to Literal types. For example:

from dataclasses import dataclass
from dataclass_wizard import JSONWizard
from typing import Literal


@dataclass
class WithValidation(JSONWizard):
    choice: Literal[1, 2, 3]


instance_a = WithValidation(4)

# these should both result in errors (not in literal value)

# instance_b = WithValidation.from_json("""{"choice": "not-an-integer"}""")
instance_c = WithValidation.from_json("""{"choice": 4}""")

The above code results in a ParseError:

Failure parsing field `choice` in class `WithValidation`. Expected a type Literal, got int.
  value: 4
  error: Value not in expected Literal values
  allowed_values: [1, 2, 3]
  json_object: '{"choice": 4}'

rnag avatar Feb 11 '22 17:02 rnag

@dustinlagoy

would be happy to work on a PR if so.

Still willing? ;)

george-zubrienko avatar Jul 20 '23 22:07 george-zubrienko