Problem:
When a Nested schema has a validation error its @post_load hooks are not invoked bypassing any transformations done there. This means the parent schema’s @validates and @validate_schema hooks have to deal with both the transformed and untransformed child values.
Originally I encountered this using marshmallow-sqlalchemy with load_instance=True.
Steps to reproduce:
The code below shows the problem.
import dataclasses
import uuid
import marshmallow as ma
import pytest
@dataclasses.dataclass
class ChildModel:
uuid: uuid.UUID
name: str
class Child(ma.Schema):
uuid = ma.fields.UUID(required=True)
name = ma.fields.String(required=True)
@ma.post_load
def transform(self, data, **_kwargs):
return ChildModel(**data)
class Parent(ma.Schema):
child = ma.fields.Nested("Child")
@ma.validates("child")
def validate_child(self, value, **_kwargs):
if value:
# This fails when the child has a validation error as then its @post_load is not called.
assert isinstance(value, ChildModel)
def test_load_valid_uuid():
data = {"child": {"uuid": "c81505b4-258b-4912-b8bc-8ac913d56736", "name": "test"}}
result = Parent().load(data)
assert isinstance(result["child"], ChildModel)
def test_load_invalid_uuid():
data = {"child": {"uuid": "invalid-uuid", "name": "test"}}
with pytest.raises(ma.exceptions.ValidationError):
result = Parent().load(data)
When the child schema validates then Parent.validate_child is called with a ChildModel instance, but if the child data is invalid it gets called with a dict instance instead.
Problem:
When a Nested schema has a validation error its
@post_loadhooks are not invoked bypassing any transformations done there. This means the parent schema’s@validatesand@validate_schemahooks have to deal with both the transformed and untransformed child values.Originally I encountered this using
marshmallow-sqlalchemywithload_instance=True.Steps to reproduce:
The code below shows the problem.
When the child schema validates then
Parent.validate_childis called with a ChildModel instance, but if the child data is invalid it gets called with a dict instance instead.