Search code examples
pythonflasksqlalchemymarshmallow

Flatten data on Marshallow / SQLAlchemy Schema


I'm having some issues with how I'm getting my data back from one of my endpoints - specifically, with Marshmallow and SQLAlchemy.

I have a many-to-many relationship between cocktails and ingredients, but I also have more data than just foreign keys on the relational table, ings_in_cocktail, such as ounces. When I GET /cocktails/, it returns something like this:

{
  "cocktails": [
    {
      "glass": "rocks",
      "ingredients": [
        {
          "ingredient": {
            "ing_type": "liquor",
            "id": 1,
            "name": "gin"
          },
          "ounces": 20
        }
      ],
      "finish": "stirred",
      "id": 1,
      "name": "gin and tonic"
    }
  ]
}

What I'd like to do is combine the spread the ounces property with the ingredient dict.

I want the data to look like the following:

{
  "cocktails": [
    {
      "glass": "rocks",
      "ingredients": [
        {
          "ing_type": "liquor",
          "id": 1,
          "name": "gin",
          "ounces": 20
        }
      ],
      "finish": "stirred",
      "id": 1,
      "name": "gin and tonic"
    }
  ]
}

After searching the web for hours, I can't find a way to do this easily with Marshmallow. Is there some easy way I'm missing?

Code

ingredients.py

from flask import Flask
from settings import db, ma

class Ingredient(db.Model):
    __tablename__ = 'ingredients'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), nullable=False)
    ing_type = db.Column(db.String(20), nullable=False)

class IngredientSchema(ma.ModelSchema):
    class Meta:
        model = Ingredient

ings_in_cocktail.py

from flask import Flask
from settings import db, ma

from models.ingredients import Ingredient, IngredientSchema

class CocktailIngredient(db.Model):
    __tablename__ = 'ings_in_cocktail'
    ing_id = db.Column(db.Integer, db.ForeignKey('ingredients.id'), primary_key=True)
    cocktail_id = db.Column(db.Integer, db.ForeignKey('cocktails.id'), primary_key=True)
    ounces = db.Column(db.Integer, nullable=False)

    ingredient = db.relationship('Ingredient')

# Necessary for transforming sqlalchemy data into serialized JSON


class CocktailIngredientSchema(ma.ModelSchema):
    ingredient = ma.Nested(IngredientSchema, strict=True)

    class Meta:
    model = CocktailIngredient

cocktails.py

from flask import Flask
from settings import db, ma

from models.ing_in_cocktails import CocktailIngredient, CocktailIngredientSchema
from models.ingredients import Ingredient, IngredientSchema

class Cocktail(db.Model):
    __tablename__ = 'cocktails'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), nullable=False)
    glass = db.Column(db.String(20), nullable=False)
    finish = db.Column(db.String(20), nullable=True)
    ingredients = db.relationship(
        'CocktailIngredient',
        # secondary='ings_in_cocktail',
        backref=db.backref('cocktails'),
        # primaryjoin=id == CocktailIngredient.cocktail_id
    )

# Necessary for transforming sqlalchemy data into serialized JSON
class CocktailSchema(ma.ModelSchema):
    # this is responsible for returning all the ingredient data on the cocktail
    ingredients = ma.Nested(CocktailIngredientSchema, many=True, strict=True)
    class Meta:
    model = Cocktail

Solution

  • I ended up solving it like this:

    class CocktailSchema(ma.ModelSchema):
        # this is responsible for returning all the ingredient data on the cocktail
        ingredients = ma.Nested(CocktailIngredientSchema, many=True, strict=True)
        ingredients = fields.Method('concat_ingredients_dicts')
    
        """
        at this point the ingredients field on the cocktail object looks something like this
    
        ingredients: [{
            ingredient: {
                name: 'white russian',
                glass: 'rocks',
                finish: 'stirred'
            },
            ounces: 2,
            action: 'muddle',
            step: 1
        }]
    
        what we want is to concat this data so "ingredients" just turns
        into an list of dicts
        """
        def concat_ingredients_dicts(self, obj):
            result_ingredients_list = []
            i = 0
            while i < len(list(obj.ingredients)):
                # create a dict from the fields that live in the relational table
                relational_fields_dict = {
                    'ounces': obj.ingredients[i].ounces,
                    'action': obj.ingredients[i].action,
                    'step': obj.ingredients[i].step
                }
    
                # create a dict from the fields on each ingredient in the cocktail
                ingredients_dict = obj.ingredients[i].ingredient.__dict__
                ingredients_dict_extracted_values = {
                    'name': ingredients_dict.get('name'),
                    'type': ingredients_dict.get('ing_type'),
                    'id': ingredients_dict.get('id')
                }
    
                # merge the two dicts together
                merged = dict()
                merged.update(ingredients_dict_extracted_values)
                merged.update(relational_fields_dict)
    
                # append this merged dict a result array
                result_ingredients_list.append(merged)
                i += 1
            # return the array of ingredients
            return result_ingredients_list
    
        class Meta:
          model = Cocktail