pythonsqlalchemyflask-sqlalchemymarshmallowflask-marshmallow

How to get marshmallow to give out a List of nested classes with 1 entry pulled out


I am trying to get flask-marshmallow to give me a list of translated entries from a Table called Type that is associated to a table called Language. I cannot figure out how to nest the responses in a list. I gave an example of my current output JSON along with my goal JSON.

class TypeSchema(ma.SQLAlchemySchema):
    class Meta:
        model = Type
        ordered = True
    language = ma.String(attribute="language.language", dump_only=True)
    translated_languages = ma.List(ma.String(attribute="language"))

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')


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

    id = sqla.Column(sqla.Integer, primary_key=True)

    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')

Here is the resulting JSON

    {
      "id": 1,
      "translated_languages": [
        "<api.models.Language object at 0x00000171D3730490>", 
        "<api.models.Language object at 0x00000171D3730400>", 
        "<api.models.Language object at 0x00000171D3730520>"
      ], 
      "language": "en", 
    }

Here is my goal JSON

    {
      "id": 1,
      "translated_languages": [
        "fr", 
        "es", 
        "de"
      ], 
      "language": "en", 
    }

Solution

  • You can do this with a combination of marshmallow, marshmallow-sqlalchemy and flask-marshmallow. Nested objects are usually serialised as dictionaries to enable easy round-tripping of the data. Since you want simple strings rather than dicts we can use marshmallow's post_dump decorator to massage the data after it has been generated.

    import pprint as pp
    
    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy
    import marshmallow as mm
    import marshmallow_sqlalchemy as ms
    import flask_marshmallow as ma
    
    app = Flask(__name__)
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
    db = SQLAlchemy(app)
    
    # Model definitions omitted.
    
    
    class LanguageSchema(ma.sqla.SQLAlchemyAutoSchema):
        class Meta:
            model = Language
        fields = ('language',)
    
    
    class TypeSchema(ma.sqla.SQLAlchemyAutoSchema):
        class Meta:
            model = Type
            include_relationships = True
    
        language = ma.fields.fields.String(attribute='language.language', dump_only=True)
        translated_languages = ms.fields.Nested(
            LanguageSchema, many=True, exclude=('id',)
        )
    
        @mm.post_dump
        def patch_translated_languages(self, data, many, **kwargs):
            """Represent translated languages as simple strings."""
            language_names = [d['language'] for d in data['translated_languages']]
            data['translated_languages'] = language_names
            return data
    
    # Flask 3.x requires app_context.
    with app.app_context():
        db.create_all()
        de, en, es, fr = [
            Language(language=lang) for lang in ['de', 'en', 'es', 'fr']
        ]
        type_ = Type(language=en, translated_languages=[fr, es, de])
        db.session.add(type_)
        db.session.commit()
    
    with app.app_context():
        type_ = Type.query.first()
        dump = TypeSchema().dump(type_)
        pp.pprint(dump)
    

    It wasn't clear to me what your imports were, so I've included mine for clarity.