Сериализатор жалуется на данные запроса

#django #django-rest-framework

#django #django-rest-framework

Вопрос:

Мне нужно добавить некоторые данные к запросу, поэтому я сделал следующее:

 data = {'my_data': 1, **request.data}
...
serializer = MySerializer(data=data)
serializer.is_valid()
 

Но сериализатор жалуется на то, что поля не в правильном формате:

«Недопустимая строка».

Он говорит то же самое для всех из них. Имеет смысл, потому что я вижу, что он создает dict, заполненный списками:

 {'attr1':[1], 'attr2':[2], ..., 'my_data':1}
 

Что не имеет смысла, так это то, что это работает просто отлично:

 serializer = MySerializer(data=request.data)
serializer.is_valid()
 

Несмотря QueryDict на то, что объект также содержит все поля, заключенные в список

Я также попробовал следующее:

 data = {'my_data': [1], **request.data}
 

Но теперь он также жалуется на новое поле.

Что я делаю не так?

Редактировать:

Просто чтобы уточнить, обходным путем было бы просто развернуть все элементы:

 data = {**{k: v for k, v in request.data.items()}}
 

Но почему сериализатор ведет себя по-разному с обычными dicts и с QueryDict s?

Комментарии:

1. Почему бы не добавить my_data request.data напрямую и затем передать его сериализатору?

Ответ №1:

QueryDict — это не просто словарь со значениями полей в виде списков, а сложная структура с переопределенными методами, которые определяют, как она ведет себя для определенных операторов. Ниже приведен метод getitem, определенный в MultiValueDict , который является базовым классом для QueryDict .

 def __getitem__(self, key):
    """
    Return the last data value for this key, or [] if it's an empty list;
    raise KeyError if not found.
    """
    try:
        list_ = super().__getitem__(key)
    except KeyError:
        raise MultiValueDictKeyError(key)
    try:
        return list_[-1]
    except IndexError:
        return []
 

При этом, несмотря на то, что значения для ключей являются списками, когда вы пытаетесь получить элемент из QueryDict, например

 my_query_dict[my_key]
 

Возвращается последний элемент в списке. Однако, когда вы создаете новый словарь, используя ** в запросе dict, вы получаете просто обычный словарь со своими элементами в виде списков, поэтому, когда вы получаете значение ключа, вы получаете обратно список. Для примера пусть my_query_dict будет экземпляром QueryDict, содержащим значение [1] для ключа ‘a’.

 my_query_dict['a'] # returns 1
{**my_query_dict}['a'] # returns [1]
 

Ответ №2:

Некоторые могут сказать, что возиться с request.data «неправильным способом». Но если вы все еще этого хотите, тогда лучше создайте изменяемую копию QueryDict:

 data = request.data.copy()
data['my_data'] = 1
 

rest_framework имеет множество миксинов / дженериков / встроенных представлений, которые просто инициализируют сериализаторы как SerializerCls(data=request.data, instance=something_or_none, context={'request': view.request, 'view': view} . И если вы используете один и тот же сериализатор в более чем одном представлении, тогда лучше сделать ваш сериализатор «умным» вместо изменения каждого представления.

«Умный» сериализатор должен вычислять все дополнительные данные из «raw request.data / request «. «Частично интеллектуальные» сериализаторы могут потребовать внесения изменений в представление.

Если вам просто нужно установить пользовательское значение для некоторого поля модели при сохранении, то вы можете изменить метод представления perform_update / perform_create (если вы используете GenericAPIView -s)

 def perform_update(self, serializer):
    serializer.save(my_data=1)
 

Если вам действительно нужны эти пользовательские данные, учитываемые во время проверки: вы можете использовать контекст:

 class MySerializer(...):
    def validate(self, data):
        if 'my_data' in self.context:
            data['my_data'] = self.context['my_data']
        if data.get('my_data'): 
            perform_some_extra_validation(data)
        return data

serializer = MySerializer(data=data, context={'my_data': 1})
serializer.is_valid()
# for ModelView-s you can change context by modifying `get_serializer_context` method

 

Если вы можете my_data динамически вычислять из request или из request.user , то:

  • Вы можете сделать это прямо в serializer.validate контексте с помощью:
 class MySerializer(...):
    def validate(self, data):
        if 'request' in self.context:
            data['my_data'] = build_my_data_from_request(self.context['request'])
        return data

serializer = MySerializer(data=data, context={'request': request})
serializer.is_valid()

# in ModelView-s serializers are already initialized with 'request' in context
 
  • Или вы можете использовать комбинацию контекста и default аргументов для поля:
 

class MyDataDefault(object):
    def set_context(self, serializer_field):
        self.parent = serializer_field.parent

    def __call__(self):
        if 'request' in self.parent.context:
            return build_my_data_from_request(self.parent.context['request'])


class MySerializer(...):
    my_data = serializers.SomeField(default=MyDataDefault())


# if my_data should only be set during creation (not during update)
from rest_framework.serializers import CreateOnlyDefault

class AlternativeMySerializer(...):
    my_data = serializers.SomeField(default=CreateOnlyDefault(MyDataDefault()))


serializer = MySerializer(data=data, context={'request': request})
serializer.is_valid()
# in ModelView-s serializers are already initialized with 'request' in context
 

Если вы используете ModelViewSet или другие представления, основанные на GenericAPIView , то get_serializer уже делает что-то похожее на SerializerCls(data=request.data, instance=..., context={'request': request, 'view': self}) :

     def get_serializer(self, *args, **kwargs):
        """
        Return the serializer instance that should be used for validating and
        deserializing input, and for serializing output.
        """
        serializer_class = self.get_serializer_class()
        kwargs['context'] = self.get_serializer_context()
        return serializer_class(*args, **kwargs)

    ...


    def get_serializer_context(self):
        """
        Extra context provided to the serializer class.
        """
        return {
            'request': self.request,
            'format': self.format_kwarg,
            'view': self
        }

 

Комментарии:

1. Почему возится с запросом. данные здесь не подходят? Я спрашиваю, потому что я использовал этот подход несколько раз раньше и всегда считал, что это самый ясный подход для таких случаев, и хочу знать, есть ли возможные проблемы, которые я не рассматривал. Можете ли вы указать на какие-либо статьи или обсуждения по этой теме?

2. В вашем случае это не так критично: вы не модифицируете напрямую request.data , а используете его только для создания нового объекта. Но что, если request.data он уже содержит 'my_data' — должен ли он быть автоматически переопределен или должна быть вызвана ошибка? Что, если вы собираетесь использовать serializer в представлении, которое использует rest_framework.mixins.* ? По умолчанию эти микшины вызывают self.get_serializer(data=request.data) . Поэтому «лучше» спроектировать ваши сериализаторы таким образом, чтобы они могли вычислять дополнительные поля без внешних изменений данных

3. Резюме: если вы используете сериализатор в одном представлении / месте, тогда ваш подход хорош (но, возможно, вам следует использовать data = request.data.copy(); data['my_data'] = 1 , потому что это QueryDict), и если вы используете сериализатор в нескольких представлениях или используете View-mixins / generic-view для rest_framework, тогда лучше «создать»умный сериализатор» (с помощью пользовательского validate метода или с использованием классов с set_context полями по умолчанию)