0

I have a table called Type that associates with a table called Language. I have a 1to1 and a 1tomany relatioship for language and translated_languages from Type to Language. When I try to pull the language out of the 1toMany relationship it gives me the objective reference.

Here is my schema setup

class TypeSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
        model = Type
        include_relationships = True
    #This works great!
    language = ma.String(attribute="language.language", dump_only=True)
    #This gives me an object reference
    translated_languages = ma.List(ma.String(attribute="language.language", dump_only=True))

Here is the output JSON language shows properly, but the translated languages fail for some reason.

    {
      "language": "en",
      "translated_languages": [
        "<api.models.Language object at 0x000002598642C8E0>", 
        "<api.models.Language object at 0x000002598642C850>", 
        "<api.models.Language object at 0x000002598642C970>"
      ]
    }

Here is my route and the model defining the associations

@types.route('/types', methods=['GET'])
@authenticate(token_auth)
@paginated_response(type_schema)
def all():
    """Retrieve all types"""
    return Type.select()


class Type(Updateable, db.Model):
    __tablename__ = 'type'

    translated_languages = sqla_orm.relationship('Language', back_populates='type', foreign_keys='Language.type_id')
    language = sqla_orm.relationship('Language', back_populates='types', uselist=False, foreign_keys='Language.types_id')


class Language(Updateable, db.Model):
    __tablename__ = 'language'

    id = sqla.Column(sqla.Integer, primary_key=True)
    language = sqla.Column(sqla.String(2), nullable=False)

    type_id = sqla.Column(sqla.Integer, sqla.ForeignKey('type.id'), index=True)
    type = sqla_orm.relationship('Type', foreign_keys='Language.type_id', back_populates='translated_languages')

    types_id = sqla.Column(sqla.Integer, sqla.ForeignKey('type.id'), index=True)
    types = sqla_orm.relationship('Type', foreign_keys='Language.types_id', back_populates='language')
rockets4all
  • 684
  • 3
  • 8
  • 32

1 Answers1

0

In marshmallow, fields.List will send the inner object to fields.String as value(see marshmallow.fields.List._serialize), while fields.String will simply convert the input value to string(see marshmallow.fields.String._serialize).

class List(Field):
    def _serialize(
        self, value, attr, obj, **kwargs
    ) -> typing.Optional[typing.List[typing.Any]]:
        if value is None:
            return None
        return [self.inner._serialize(each, attr, obj, **kwargs) for each in value]

class String(Field):
    def _serialize(self, value, attr, obj, **kwargs) -> typing.Optional[str]:
        if value is None:
            return None
        return utils.ensure_text_type(value)

This is why your objects in translated_languages shown as "<api.models.Language object at 0x000002598642C8E0>".

In order to dump as expect, we can use a custom field which is inherited from fields.List and override it's _serialize function.

There is an example for you. CustomInnerSerializerListField is the custom field, and in the dump results the inner_names to inner_names6 are all my failed attempts, inner_names_expected is the exatily result I want.

import typing
from flask_marshmallow import Marshmallow
from marshmallow import fields

ma = Marshmallow()


class SelfNestedModule:
    def __init__(self, name, outer_id=0, inner_list=None):
        self.name = name
        assert not (outer_id and inner_list)
        self.outer_id = outer_id
        self.id = id(self)

        # define self-referential relationship in sqlalchemy
        self.outer: SelfNestedModule = None  # relationship
        self.inner_list = inner_list or []  # backref

    @property
    def role(self):
        assert not (self.outer_id and self.inner_list)
        if self.outer_id:
            return 'Inner'
        return 'Outer'

    def add_inner(self, inner: 'SelfNestedModule'):
        assert self.role == 'Outer'
        self.inner_list.append(inner)

    def save_to_outer(self, outer_obj: 'SelfNestedModule'):
        # self.role: Outer -> Inner
        self.outer_id = outer_obj.id
        outer_obj.add_inner(self)
        self.outer = outer_obj
        return self

    def __repr__(self):
        new_line = '\n' if self.role == 'Outer' else ''
        return (
            f'{self.role}({new_line}'
            f' name="{self.name}", {new_line}'
            f' outer_name="{self.outer.name if self.outer else ""}", {new_line}'
            f' inner_list=[%s]{new_line}'
            f')  # id={self.id}'
        ) % (('\n' + '\n'.join(map(lambda o: '  ' + repr(o), self.inner_list)) + '\n') if self.inner_list else '')

    def __str__(self):
        return f'<SelfNestedModule object at {id(self)}>'


outer = SelfNestedModule("outer_obj")
inner1 = SelfNestedModule("inner_obj1").save_to_outer(outer)
inner2 = SelfNestedModule("inner_obj2").save_to_outer(outer)
another_outer = SelfNestedModule("another_outer_obj")

print(repr(outer))
print(repr(another_outer))
"""
Outer(
 name="outer_obj", 
 outer_name="", 
 inner_list=[
  Inner( name="inner_obj1",  outer_name="outer_obj",  inner_list=[])  # id=140663262984120
  Inner( name="inner_obj2",  outer_name="outer_obj",  inner_list=[])  # id=140663262986024
]
)  # id=140663262985576
Outer(
 name="another_outer_obj", 
 outer_name="", 
 inner_list=[]
)  # id=140663263074680
"""

"""
We need rows in table be dumped as:
{
    {
      'id': 140663262985576,
      'name': 'outer_obj',
      'inner_names': [
        'inner_obj1',
        'inner_obj2',
      ]
    },
    {
      'id': 140663262984120,
      'name': 'inner_obj1',
      'outer_name': 'outer_obj',
      'inner_names': []
    },
    {
      'id': 140663262986024,
      'name': 'inner_obj2',
      'outer_name': 'outer_obj',
      'inner_names': []
    },
    {
      'id': 140663263074680,
      'name': 'another_outer_obj',
      'inner_names': []
    },
}
"""


class CustomInnerSerializerListField(fields.List):
    def __init__(self, cls_or_instance: typing.Union[fields.Field, type], inner_serializer=None, **kwargs):
        super().__init__(cls_or_instance, **kwargs)
        import functools
        self.inner_serializer_fun = functools.partial(inner_serializer, self) if inner_serializer else self.inner._serialize

    def _nested_inner_serializer(self, inner_value, outer_attr, outer_obj, accessor=None, **kwargs):
        attr = self.inner.attribute or outer_attr
        return self.inner.serialize(attr, inner_value, accessor=accessor, **kwargs)

    def _serialize(
        self, value, attr, obj, **kwargs
    ) -> typing.Optional[typing.List[typing.Any]]:
        if value is None:
            return None
        return [self.inner_serializer_fun(each, attr, obj, **kwargs) for each in value]


class MySchema(ma.SQLAlchemyAutoSchema):
    class Meta:
        model = None

    id = ma.Int(dump_only=True)
    name = ma.Str(attribute='name')
    outer_name = ma.Str(attribute='outer.name')
    inner_names = ma.List(ma.Str(attribute='name'), dump_only=True, attribute='inner_list')
    inner_names2 = ma.List(ma.Nested('self', only=('name', )), dump_only=True, attribute='inner_list')
    inner_names3 = ma.List(ma.Str(attribute='name'), dump_only=True, attribute='inner_list.name')
    inner_names4 = ma.List(ma.Str(attribute='inner_list.name'), dump_only=True, attribute='inner_list')
    inner_names5 = ma.List(ma.Str(accessor=lambda obj: obj.name), dump_only=True, attribute='inner_list')
    inner_names6 = ma.List(ma.Str(accessor=lambda obj: obj.name), dump_only=True, attribute='inner_list')
    inner_names_expected = CustomInnerSerializerListField(ma.Str(attribute='name'), dump_only=True, attribute='inner_list', inner_serializer=CustomInnerSerializerListField._nested_inner_serializer)
    inner_names_original = CustomInnerSerializerListField(ma.Str(attribute='name'), dump_only=True, attribute='inner_list')
    # outer = ma.Nested('self', exclude=('outer', ))
    # inner_list = ma.List(ma.Nested('self', exclude=('outer', )))


schema = MySchema()
schema.dump(outer)
schema.dump(another_outer)
schema.dump(inner1)
"""
schema.dump(outer)
{'id': 140663262985576, 'inner_names5': ['<SelfNestedModule object at 140663262984120>', '<SelfNestedModule object at 140663262986024>'], 'inner_names_expected': ['inner_obj1', 'inner_obj2'], 'name': 'outer_obj', 'inner_names_original': ['<SelfNestedModule object at 140663262984120>', '<SelfNestedModule object at 140663262986024>'], 'inner_names6': ['<SelfNestedModule object at 140663262984120>', '<SelfNestedModule object at 140663262986024>'], 'inner_names4': ['<SelfNestedModule object at 140663262984120>', '<SelfNestedModule object at 140663262986024>'], 'inner_names2': [{'name': 'inner_obj1'}, {'name': 'inner_obj2'}], 'inner_names': ['<SelfNestedModule object at 140663262984120>', '<SelfNestedModule object at 140663262986024>']}
schema.dump(another_outer)
{'id': 140663263074680, 'inner_names5': [], 'inner_names_expected': [], 'name': 'another_outer_obj', 'inner_names_original': [], 'inner_names6': [], 'inner_names4': [], 'inner_names2': [], 'inner_names': []}
schema.dump(inner1)
{'id': 140663262984120, 'outer_name': 'outer_obj', 'inner_names5': [], 'inner_names_expected': [], 'name': 'inner_obj1', 'inner_names_original': [], 'inner_names6': [], 'inner_names4': [], 'inner_names2': [], 'inner_names': []}
"""