Search code examples
pythonflasksqlalchemymarshmallow

Marshmallow deserializing fails when structure is nested


I am trying to deserialize a deep structure with marshmallow. For example:

hour = {
    'day': {
        'name': 'monday'
    }
}
loaded_hour, error = HoursSerializationSchema().load(hour) # this works

new_practitioner_at_location = {
    'hours': [
        hour
    ]
}
loaded, error = PractitionerToServiceLocationSerializationSchema().load(new_practitioner_at_location) # this fails

When I try to deserialize the new_practitioner_at_location I get the following (occurs when the serializer is working on the 'day' key):

AttributeError: 'dict' object has no attribute '_sa_instance_state'

Note that the same schema works to deserialize the same data structure (hour) when that structure is not nested inside the new_practitioner_at_location.

self-contained script showing the problem:

from sqlalchemy import Column, Integer, ForeignKey, String
from sqlalchemy.orm import relationship, backref
from sqlalchemy.ext.declarative import declarative_base
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from flask_migrate import Migrate

base = declarative_base()


class HoursDay(base):
    __tablename__ = 'HoursDay'
    uid = Column(Integer, primary_key=True)

    name = Column(String)

    hour_id = Column(Integer, ForeignKey('Hours.uid'))
    hour = relationship("Hours", back_populates="day")

    def __init__(self, **kwargs):
        super().__init__(**kwargs)


class Hours(base):
    __tablename__ = 'Hours'
    uid = Column(Integer, primary_key=True)

    practitioner_at_location_id = Column(Integer, ForeignKey('PractitionerToServiceLocation.uid'))
    practitioner_at_location = relationship('PractitionerToServiceLocation', back_populates="hours")

    day = relationship(HoursDay, uselist=False, back_populates="hour")

    def __repr__(self):
        return f'<Hours {self.uid}>'


class PractitionerToServiceLocation(base):
    """
    A practitioner practices at a number of service locations.
    """
    __tablename__ = 'PractitionerToServiceLocation'
    uid = Column(Integer, primary_key=True)

    hours = relationship("Hours", back_populates="practitioner_at_location")

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def __repr__(self):
        return f'<PractitionerToServiceLocation {self.uid}>'


app = Flask(__name__)
app.config.from_object(os.environ['APP_SETTINGS'])
db = SQLAlchemy(app, model_class=base)
ma = Marshmallow(app)
migrate = Migrate(app, db)

from marshmallow import fields


class HoursDaySerializationSchema(ma.ModelSchema):
    class Meta:
        model = HoursDay


class HoursSerializationSchema(ma.ModelSchema):
    class Meta:
        model = Hours

    day = fields.Nested(HoursDaySerializationSchema)


class PractitionerToServiceLocationSerializationSchema(ma.ModelSchema):
    class Meta:
        model = PractitionerToServiceLocation

        hours = fields.Nested('HoursSerializationSchema', many=True)


if __name__ == "__main__":
    hour = {
        'day': {
            'name': 'monday'
        }
    }
    loaded_hour, error = HoursSerializationSchema().load(hour) # this works

    new_practitioner_at_location = {
        'hours': [
            hour
        ]
    }
    loaded, error = PractitionerToServiceLocationSerializationSchema().load(new_practitioner_at_location) # this fails
    print('hi')

Update:

I think that what is happening is that marshmallow is not attempting to deserialize the HoursDay object when trying to deserialize the new_practitioner_at_location dict. If I remove the backpopulates behavior from the HoursDay.hour field then you can see it just assign the unserialized data struct to the field. This makes no sense at all to me, especially since it works when you just deserialize the hour dict directly instead of embedding it inside new_practitioner_at_location. Any help would be appreciated.


Solution

  • This is a simple typo-like bug:

    class PractitionerToServiceLocationSerializationSchema(ma.ModelSchema):
        class Meta:
            model = PractitionerToServiceLocation
    
            hours = fields.Nested('HoursSerializationSchema', many=True)
    

    You are defining hours inside class Meta, but it needs to be in your schema itself:

    class PractitionerToServiceLocationSerializationSchema(ma.ModelSchema):
        class Meta:
            model = PractitionerToServiceLocation
    
        hours = fields.Nested('HoursSerializationSchema', many=True)