lovasoa / marshmallow_dataclass

Automatic generation of marshmallow schemas from dataclasses.

Home Page:https://lovasoa.github.io/marshmallow_dataclass/html/marshmallow_dataclass.html

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Setting marshmallow_field metadata attribute with required=True raises TypeError instead of ValidationError when field is missing

jeancochrane opened this issue · comments

When required=True is set in on a field in its dataclasses.field.metadata dictionary, I would expect the package to raise ValidationError when validating the schema when the field is missing. Instead, the package seems to raise a TypeError.

Here's a reproducible example of the issue:

from dataclasses import field

from marshmallow import ValidationError
from marshmallow.fields import Url
from marshmallow_dataclass import dataclass


@dataclass
class CorrectValidationError:
    """dataclass *without* marshmallow_field; validation works as expected"""
    url: str = field(metadata={"required": True})


@dataclass
class IncorrectValidationError:
    """dataclass *with* marshmallow_field; validation raises TypeError"""
    url: str = field(metadata={"required": True, "marshmallow_field": Url()})


if __name__ == "__main__":
    print("Correct validation error:")
    try:
        CorrectValidationError.Schema().load({})
    except ValidationError as e:
        print("ValidationError:", e.messages)

    print("Incorrect validation error:")
    try:
        IncorrectValidationError.Schema().load({})
    except TypeError as e:
        print("TypeError:", e)

Output:

Correct validation error:
ValidationError: {'url': ['Missing data for required field.']}
Incorrect validation error:
TypeError: __init__() missing 1 required positional argument: 'url'

I'd be happy to put up a PR to fix this if it would be helpful!

Just did a little bit of messing about to figure out a workaround. I tried adding a field validator with @validates, but it seems like the object is initialized before the field validator runs, so we still get a TypeError:

Incorrect validation error:
TypeError: __init__() missing 1 required positional argument: 'url'

@validates_schema seems to work, however:

@dataclass
class IncorrectValidationError:
    url: str = field(metadata={"required": True, "marshmallow_field": Url()})

    @validates_schema
    def validate_schema(self, data: Dict, **kwargs: Any) -> None:
        if "url" not in data:
            raise ValidationError({"url": ["Missing data for required field."]})
Incorrect validation error:
ValidationError: {'url': ['Missing data for required field.']}

Only downside is that it seems like validates_schema runs after all other required checks have been completed, so if the caller is missing other required fields, they won't see the field validated by the schema in the output unless all other fields are provided:

from dataclasses import field
from typing import Any, Dict

from marshmallow import validates_schema, ValidationError
from marshmallow.fields import Url
from marshmallow_dataclass import dataclass


@dataclass
class IncorrectValidationError:
    foo: str = field(metadata={"required": True})
    url: str = field(metadata={"required": True, "marshmallow_field": Url()})

    @validates_schema
    def validate_schema(self, data: Dict, **kwargs: Any) -> None:
        if "url" not in data:
            raise ValidationError({"url": ["Missing data for required field."]})


if __name__ == "__main__":
    print("Incorrect validation error, missing both arguments:")
    try:
        IncorrectValidationError.Schema().load({})
    except ValidationError as e:
        print("ValidationError:", e.messages)

    print("Incorrect validation error, missing only url:")
    try:
        IncorrectValidationError.Schema().load({"foo": "bar"})
    except ValidationError as e:
        print("ValidationError:", e.messages)
Incorrect validation error, missing both arguments:
ValidationError: {'foo': ['Missing data for required field.']}
Incorrect validation error, missing only url:
ValidationError: {'url': ['Missing data for required field.']}

This is an improvement over getting a TypeError for my purposes, at least!

On further reflection, I don't actually think this is a bug -- I think the object instantiated in marshmallow_field just needs to have the same value of required as the field() kwarg does, e.g.:

from dataclasses import field

from marshmallow import ValidationError
from marshmallow.fields import Url
from marshmallow_dataclass import dataclass


@dataclass
class RaiseValidationError:
    """dataclass *with* marshmallow_field; validation raises ValidationError"""
    url: str = field(metadata={"required": True, "marshmallow_field": Url(required=True)})


if __name__ == "__main__":
    print("Correct validation error:")
    try:
        RaiseValidationError.Schema().load({})
    except ValidationError as e:
        print("ValidationError:", e.messages)
Correct validation error:
ValidationError: {'url': ['Missing data for required field.']}