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

Django REST Framework更新用户时出现KeyError: 'centros'问题求助

问题描述

使用Django REST Framework开发应用时,更新用户时抛出KeyError: 'centros',错误触发点为centros = validated_data.pop('centros')代码行。

模型代码

class Usuario(AbstractBaseUser):
    organizacion = models.CharField(verbose_name='Organización', blank=True, null=True, max_length=250)
    departamento = models.CharField(verbose_name='Departamento', blank=True, null=True, max_length=250)
    centros = models.ManyToManyField(Centro, verbose_name='Centro o Unidad', blank=True, related_name='centros_usuario')
    telefono = models.CharField(max_length=100, verbose_name='Teléfono', blank=True, null=True)
    rut = models.CharField(max_length=100, verbose_name='RUT', blank=True, null=True)
    profesion = models.CharField(max_length=150, verbose_name='Profesión', blank=True, null=True)
    cargo = models.CharField(max_length=150, verbose_name='Cargo', blank=True, null=True)
    nacionalidad = models.CharField(max_length=100, verbose_name='Nacionalidad', blank=True, null=True)
    sexo = models.CharField(max_length=10, choices=SEXO_CHOICES ,verbose_name='Sexo', blank=True, null=True)
    fecha_nacimiento = models.DateField(auto_now=False, auto_now_add=False,verbose_name='Fecha de nacimiento', blank=True, null=True)
    imagen_perfil = models.ImageField(upload_to='perfil/', verbose_name='Imágen de Perfil', max_length=255, blank=True, null=True)
    cv = models.FileField(verbose_name='CV', upload_to='cv_usuarios/', blank=True, null=True)
    direccion = models.CharField(max_length=250, blank=True, null=True, verbose_name='Dirección')
    perfil = models.PositiveSmallIntegerField(
        null=False,
        blank=False,
        default=2,
        choices=PERFIL_CHOICES,
        verbose_name='Perfil')
    is_active = models.BooleanField(
        verbose_name='active',
        default=True,
        help_text=(
            'Designates whether this user shoud be treated as active. '
            'Unselect this instead of deleting accounts. '
        )
    )
    estado = models.CharField(verbose_name='Estado usuario', default='INHABILITADO', choices=ESTADO_CHOICES, max_length=13)
    is_staff = models.BooleanField(
        verbose_name='staff status',
        default=False,
        help_text=(
            'Designates whether the user can log into this admin site. '
        )
    )
    is_superuser = models.BooleanField(
        verbose_name='superuser status',
        default=False,
        help_text=(
            'Designates whether this user is a Superuser. '
        )
    )
    terminos_y_condiciones = models.BooleanField(verbose_name='Términos y Condiciones', default=False)
    linkedin = models.CharField(verbose_name='Linkedin', max_length=100, blank=True, null=True)
    email = models.EmailField(max_length=250, unique=True, verbose_name='Correo')
    first_name = models.CharField(max_length=250, verbose_name='Nombre')
    last_name = models.CharField(max_length=250, verbose_name='Apellido', blank=True, null=True)

    objects = UsuarioManager()

    USERNAME_FIELD = 'email'

    def __str__(self):
        return self.first_name
    
    def get_email_user(self):
        return self.email.lower()
    
    def save(self, *args, **kwargs):
        self.email = self.email.lower()
        return super(Usuario, self).save(*args, **kwargs)

更新用户序列化器代码

class UpdateUsuarioSerializer(serializers.ModelSerializer):
    email = serializers.CharField(max_length=250, required=False)
    first_name = serializers.CharField(max_length=250, required=False)
    class Meta:
        model = Usuario
        fields = ['organizacion',
                  'departamento',
                  'centros',
                  'telefono',
                  'rut',
                  'profesion',
                  'cargo',
                  'nacionalidad',
                  'sexo',
                  'fecha_nacimiento',
                  'imagen_perfil',
                  'cv',
                  'email',
                  'first_name',
                  'last_name',
                  'linkedin'
                  ]
    
    def update(self, instance, validated_data):
        with transaction.atomic():
            centros = validated_data.pop('centros')
            instance = super(UpdateUsuarioSerializer, self).update(instance,validated_data)
            instance.save()
            instance.centros.clear()

            
            for centro in centros:
                instance.centros.add(Centro.objects.get(id=centro))
            
                    
            return instance

视图集代码

class UsuarioViewSet(viewsets.ModelViewSet):
    queryset = Usuario.objects.filter(is_active = True)
    permission_classes = [permissions.AllowAny]
    serializer_class = UsuarioSerializer
    pagination_class = StandardResultsSetPagination
    filter_backends = [DjangoFilterBackend, filters.SearchFilter]
    filterset_fields = ['perfil']
    search_fields = ['first_name', 'last_name', 'email', 'rut', 'telefono', 'profesion', 'cargo', 'centros__nombre']

    def create(self, request):
        imagen_perfil = request.data.get('imagen_perfil', None) or None
        if imagen_perfil is not None:
            try:
                request.data['imagen_perfil'] = file64bits(imagen_perfil)
            except ValueError as error:
                raise serializers.ValidationError({'imagen_perfil': 'Error en formato de archivo'})
        cv = request.data.get('cv', None) or None
        if cv is not None:
            try:
                request.data['cv'] = file64bits(cv)
            except ValueError as error:
                raise serializers.ValidationError({'cv': 'Error en formato de archivo'})
        serializer = UsuarioSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def update(self, request, pk=None):
        imagen_perfil = request.data.get('imagen_perfil', None) or None
        if imagen_perfil is not None:
            try:
                request.data['imagen_perfil'] = file64bits(imagen_perfil)
            except ValueError as error:
                raise serializers.ValidationError({'imagen_perfil': 'Error en formato de archivo'})
        cv = request.data.get('cv', None) or None
        if cv is not None:
            try:
                request.data['cv'] = file64bits(cv)
            except ValueError as error:
                raise serializers.ValidationError({'cv': 'Error en formato de archivo'})

        instance = Usuario.objects.get(id=pk)
        serializer = UpdateUsuarioSerializer(instance, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_202_ACCEPTED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
解决方案

错误核心原因是更新请求未传入centros字段,或该字段未通过序列化器验证,导致validated_data字典中无centros键,直接调用pop()触发KeyError。可通过以下方式修复:

1. 兼容centros不存在的场景

在update方法中,给pop()设置默认值,避免触发异常:

def update(self, instance, validated_data):
    with transaction.atomic():
        # 若validated_data无centros,默认返回空列表
        centros = validated_data.pop('centros', [])
        instance = super(UpdateUsuarioSerializer, self).update(instance, validated_data)
        instance.centros.clear()
        
        for centro in centros:
            instance.centros.add(Centro.objects.get(id=centro))
        
        return instance

若需保留用户原有关联的Centro,删除instance.centros.clear()即可。

2. 显式定义ManyToMany字段序列化规则

默认ModelSerializer对ManyToMany字段的处理可能不符合预期,显式指定字段类型并允许为空:

class UpdateUsuarioSerializer(serializers.ModelSerializer):
    email = serializers.CharField(max_length=250, required=False)
    first_name = serializers.CharField(max_length=250, required=False)
    # 显式定义centros字段,自动转换ID为实例
    centros = serializers.PrimaryKeyRelatedField(
        queryset=Centro.objects.all(),
        many=True,
        required=False
    )
    
    class Meta:
        model = Usuario
        fields = ['organizacion', 'departamento', 'centros', 'telefono', 'rut', 'profesion', 'cargo', 'nacionalidad', 'sexo', 'fecha_nacimiento', 'imagen_perfil', 'cv', 'email', 'first_name', 'last_name', 'linkedin']
    
    def update(self, instance, validated_data):
        with transaction.atomic():
            centros = validated_data.pop('centros', [])
            instance = super().update(instance, validated_data)
            instance.centros.clear()
            
            for centro in centros:
                instance.centros.add(centro)
            
            return instance

此方式无需再手动查询Centro实例,效率更高。

3. 支持部分字段更新

若接口允许仅更新部分字段(即PATCH请求),在实例化序列化器时添加partial=True:

def update(self, request, pk=None):
    # ... 其他代码 ...
    instance = Usuario.objects.get(id=pk)
    # 允许部分字段更新
    serializer = UpdateUsuarioSerializer(instance, data=request.data, partial=True)
    if serializer.is_valid():
        serializer.save()
        return Response(serializer.data, status=status.HTTP_202_ACCEPTED)
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

配合pop('centros', []),即使请求未传centros字段,代码也能正常执行。

内容的提问来源于stack exchange,提问作者Angel Daniel Ascanio Salazar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 19:54:59