#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
полями по умолчанию)