You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Serializer写入Recipe时触发AttributeError问题排查

Django序列化器错误:'Ingredient'对象无'amount'属性

错误信息

AttributeError: 尝试在序列化器RecipeIngredientWriteSerializer上获取字段"amount"的值时出现AttributeError。序列化器字段名称可能不正确,与"Ingredient"实例的任何属性或键都不匹配。原始异常信息:'Ingredient'对象没有属性"amount"。

模型代码

class Ingredient(models.Model):
    name = models.CharField(
        max_length=constants.MAX_NAME_LENGTH,
        verbose_name='Название'
    )
    measurement_unit = models.CharField(
        max_length=constants.MAX_NAME_LENGTH,
        verbose_name='Единица измерения'
    )

    class Meta:
        verbose_name = 'Ингредиент'
        verbose_name_plural = 'Ингредиенты'

    def __str__(self):
        return self.name


class Recipe(models.Model):
    name = models.CharField(
        max_length=constants.MAX_NAME_LENGTH,
        verbose_name='Название'
    )
    text = models.TextField(
        verbose_name='Описание'
    )
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        verbose_name='Автор',
        related_name='recipes'
    )
    cooking_time = models.PositiveSmallIntegerField(
        verbose_name='Время приготовления (в минутах)',
        validators=[
            MinValueValidator(
                1,
                'Допустимое значение не меньше 1'
            )
        ]
    )
    tags = models.ManyToManyField(
        Tag,
        verbose_name='Тег',
        through='RecipeTag'
    )
    ingredients = models.ManyToManyField(
        Ingredient,
        verbose_name='Ингредиенты',
        through='RecipeIngredient'
    )
    image = models.ImageField(
        upload_to='recipe/images/',
        null=True,
        default=None,
        verbose_name='Картинка'
        )
    pub_date = models.DateTimeField(
        verbose_name='Дата публикации',
        auto_now_add=True
    )

    class Meta:
        ordering = ['-pub_date']
        verbose_name = 'рецепт'
        verbose_name_plural = 'рецепты'

    def __str__(self):
        return self.name


class RecipeIngredient(models.Model):
    recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
    ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE)
    amount = models.IntegerField(
        verbose_name='количество ингредиента',
        validators=[
            MinValueValidator(
                1,
                'Допустимое значение не меньше 1'
            )
        ]
    )

    class Meta:
        verbose_name = 'ингредиент'
        verbose_name_plural = 'ингредиенты'

写入序列化器代码

class RecipeIngredientWriteSerializer(serializers.Serializer):
    id = serializers.IntegerField(required=True)
    amount = serializers.IntegerField(required=True)

    def validate_amount(self, value):
        if value <= 0:
            raise serializers.ValidationError(
                'Количесто ингредиента не может быть меньше 0'
            )
        return value

    class Meta:
        model = RecipeIngredient
        fields = (
            'id',
            'amount'
        )


class RecipeWriteSerializer(serializers.ModelSerializer):
    tags = serializers.PrimaryKeyRelatedField(
        many=True, queryset=Tag.objects.all()
    )
    image = Base64ImageField(required=True)
    ingredients = RecipeIngredientWriteSerializer(many=True)

    class Meta:
        model = Recipe
        fields = (
            'ingredients',
            'tags',
            'image',
            'name',
            'text',
            'cooking_time'
        )

    def create(self, validated_data):
        ingredients = validated_data.pop('ingredients')
        tags = validated_data.pop('tags')
        recipe = Recipe.objects.create(**validated_data)
        for tag in tags:
            RecipeTag.objects.create(
                tag=tag, recipe=recipe)
        for ingredient in ingredients:
            current_ingredient = get_object_or_404(
                Ingredient, id=ingredient['id']
            )
            RecipeIngredient.objects.get_or_create(
                ingredient=current_ingredient,
                amount=ingredient['amount'],
                recipe=recipe
            )
        return recipe

请求示例

{
  "ingredients": [
    {
      "id": 1123,
      "amount": 10
    }
  ],
  "tags": [
    1,
    2
  ],
  "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAgMAAABieywaAAAACVBMVEUAAAD///9fX1/S0ecCAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAACklEQVQImWNoAAAAggCByxOyYQAAAABJRU5ErkJggg==",
  "name": "string",
  "text": "string",
  "cooking_time": 1
}

用户疑问

此前读取数据时遇到过同类错误,通过为amount字段添加source参数解决,但该方法在写入场景无效,请问我的代码存在什么问题?


问题根源

当RecipeWriteSerializer完成创建并返回recipe实例后,会自动序列化该实例以返回给前端。此时ingredients字段关联的是Recipe模型的多对多字段,获取的是Ingredient对象集合,但你使用的RecipeIngredientWriteSerializer期望处理包含amount字段的RecipeIngredient对象(中间表实例),而Ingredient本身没有amount属性,因此触发错误。

修复方案

方案1:将ingredients设为仅写入字段(简单快捷)

如果创建成功后不需要返回食材数据,直接给ingredients字段添加write_only=True,让序列化器在返回数据时忽略该字段,避免尝试序列化Ingredient实例:

class RecipeWriteSerializer(serializers.ModelSerializer):
    tags = serializers.PrimaryKeyRelatedField(
        many=True, queryset=Tag.objects.all()
    )
    image = Base64ImageField(required=True)
    # 添加 write_only=True 标记
    ingredients = RecipeIngredientWriteSerializer(many=True, write_only=True)

    # 其余代码保持不变

方案2:调整序列化器以返回带amount的食材数据

如果需要在创建成功后返回包含数量的食材信息,需要拆分写入和读取逻辑:

  1. 优化RecipeIngredientWriteSerializer:去掉不必要的Meta类(因为它继承自Serializer而非ModelSerializer)
class RecipeIngredientWriteSerializer(serializers.Serializer):
    id = serializers.IntegerField(required=True)
    amount = serializers.IntegerField(required=True)

    def validate_amount(self, value):
        if value <= 0:
            raise serializers.ValidationError(
                'Количество ингредиента не может быть меньше 0'
            )
        return value
  1. 修改RecipeWriteSerializer,拆分写入用的字段和返回用的字段:
class RecipeWriteSerializer(serializers.ModelSerializer):
    tags = serializers.PrimaryKeyRelatedField(
        many=True, queryset=Tag.objects.all()
    )
    image = Base64ImageField(required=True)
    # 用于返回带数量的食材数据
    ingredients = serializers.SerializerMethodField()
    # 用于接收写入的食材数据,标记为仅写入
    ingredients_data = RecipeIngredientWriteSerializer(many=True, write_only=True, source='ingredients')

    class Meta:
        model = Recipe
        fields = (
            'ingredients_data',
            'ingredients',
            'tags',
            'image',
            'name',
            'text',
            'cooking_time'
        )

    def get_ingredients(self, obj):
        # 从中间表获取关联数据并组装返回格式
        recipe_ingredients = obj.recipeingredient_set.all()
        return [
            {
                'id': ri.ingredient.id,
                'amount': ri.amount,
                'name': ri.ingredient.name,
                'measurement_unit': ri.ingredient.measurement_unit
            } for ri in recipe_ingredients
        ]

    def create(self, validated_data):
        # 从validated_data中取出食材数据(source指定为'ingredients',所以键是'ingredients')
        ingredients = validated_data.pop('ingredients')
        tags = validated_data.pop('tags')
        recipe = Recipe.objects.create(**validated_data)
        for tag in tags:
            RecipeTag.objects.create(tag=tag, recipe=recipe)
        for ingredient in ingredients:
            current_ingredient = get_object_or_404(Ingredient, id=ingredient['id'])
            RecipeIngredient.objects.get_or_create(
                ingredient=current_ingredient,
                amount=ingredient['amount'],
                recipe=recipe
            )
        return recipe

内容的提问来源于stack exchange,提问作者Maivery

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 21:47:06