I have run into the following problem:
There are 2 serializers CreateSerializer(child) and BulkCreateSerializer(parent).
They are connected via list_serializer_class.
I have overridden create() and validate() methods for both serializers and expect them to trigger respectively on whether a single instance is coming via POST or a list of instances.
However, when I am sending a post request with a list of instances serializer does switch to many=true but uses validate() that belongs to child CreateSerializer instead of dedicated BulkCreateSerializer, which runs me into errors of course.
So my question is, what could be the logic under the hood, that prevents my serializers to distribute the items for validation respectively? And how to make it work that way?
serializers.py
class RecipeStepDraftBulkCreateSerializer(serializers.ListSerializer):
def validate(self, data):
print("bulk validate")
new_step_numbers = [s.get('step_number') for s in data]
if None in new_step_numbers:
raise serializers.ValidationError("step_number filed is required")
if new_step_numbers != list(set(new_step_numbers)):
raise serializers.ValidationError("Wrong order of step_number(s) supplied")
try:
recipe = Recipe.objects.get(pk=self.context.get('recipe_id'))
existing_steps = recipe.recipe_steps.get_queryset().all()
if existing_steps:
ex_step_numbers = [s.step_number for s in existing_steps]
if new_step_numbers[0] != ex_step_numbers[-1] + 1:
raise serializers.ValidationError(
f"The next first supplied step_number must be: {ex_step_numbers[-1] + 1}")
steps_combined = ex_step_numbers + new_step_numbers
if steps_combined != list(set(steps_combined)):
raise serializers.ValidationError(f"Wrong order of step_number(s) supplied")
return data
except ObjectDoesNotExist:
raise serializers.ValidationError("Recipe under provided id doesn't exist.")
def create(self, validated_data):
recipe = Recipe.objects.get(pk=self.context.get('recipe_id'))
for step in validated_data:
step['recipe'] = recipe
RecipeStep.objects.create(**step)
return validated_data
class RecipeStepDraftCreateSerializer(serializers.ModelSerializer):
class Meta:
model = RecipeStep
fields = [
'id',
'step_number',
'step_image',
'instruction',
'tip']
list_serializer_class = RecipeStepDraftBulkCreateSerializer
def validate(self, data):
print("single validate")
if not data.get("step_number"):
raise serializers.ValidationError("step_number field is required.")
try:
recipe = Recipe.objects.get(pk=self.context.get('recipe_id'))
existing_steps = recipe.recipe_steps.get_queryset().all()
if existing_steps:
ex_step_numbers = [s.step_number for s in existing_steps]
if data["step_number"] != ex_step_numbers[-1] + 1:
raise serializers.ValidationError(
f"The next first supplied step_number must be: {ex_step_numbers[-1] + 1}")
if data["step_number"] != 1:
raise serializers.ValidationError(f"Wrong step_number. 1 expected, got {data['step_number']}")
return data
except ObjectDoesNotExist:
raise serializers.ValidationError("Recipe under provided id doesn't exist.")
def create(self, validated_data):
recipe = Recipe.objects.get(pk=self.context.get('recipe_id'))
validated_data['recipe'] = recipe
step = RecipeStep.objects.create(**validated_data)
return step
views.py
class DraftsRecipeStepsCreateView(APIView):
serializer_class = RecipeStepDraftCreateSerializer
def post(self, request, *args, **kwargs):
print(f'DATA IS LIST: {isinstance(request.data, list)}')
serializer = self.serializer_class(
data=request.data,
many=isinstance(request.data, list),
context={
'request': request,
'recipe_id': kwargs.get('recipe_id')})
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
The code that explains this behavior can be found here. It describes how a list serializer runs validations (the run_validation method).
Based on that, the list serializer first validates each child serializer as seen in the to_internal_value implementation:
def to_internal_value(self, data):
...
for item in data:
try:
validated = self.child.run_validation(item)
except ValidationError as exc:
errors.append(exc.detail)
else:
ret.append(validated)
errors.append({})
...
before running its own list validate:
def run_validation(self, data=empty):
...
value = self.to_internal_value(data) # <-- children validation first
try:
self.run_validators(value) # <-- then the list validation
...
I tried this out with the below serializer:
class MyModelListSerializer(serializers.ListSerializer):
def validate(self, attrs):
print('mymodel list validate')
return attrs
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = '__all__'
list_serializer_class = MyModelListSerializer
def validate(self, attrs):
print('mymodel validate')
return attrs
using some sample data, and then validating:
somedata = [
{
"data": "1",
},
{
"data": "2",
}
]
s = MyModelSerializer(data=somedata, many=True); s.is_valid();
The output I get is then consistent on what the source suggests:
mymodel validate
mymodel validate
mymodel list validate
In your case I suspect print("bulk validate") did not trigger because there were problems in validating the child serializers causing the list serializer to not be run anymore.
Related
can I return a list in the to_representation method in Django Rest Framework
I need to modify the output in the serializer here is a recent output
{
"id": 1,
"display_sequence": 2
}
I need to modify the recent output to
[
{
"id": 1
"display_sequence": 2
},
{
"id" : 2
"display_sequence": 1
}
]
so the second data I got from the query filter based on the container id and target_container_id
instance = Container.objects.filter(id__in=[self.container_id, self.target_container_id])
if I return to serializer I got this error
Got AttributeError when attempting to get a value for field `display_sequence` on serializer `ContainerSequenceSwapSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `list` instance.
Original exception text was: 'list' object has no attribute 'display_sequence'.
how to I can return yo expected output?
here are my views
Views
class ContainerViewSet(viewsets.ModelViewSet):
"""Container ViewSet for CRUD operations."""
queryset = Container.objects.all()
def get_serializer_class(self):
return ContainerSerializerV1
#action(methods=["patch"], detail=True, url_path="swap-sequence")
def swap_sequence(self, request, pk):
# serializer = ContainerSequenceSwapSerializer(data=request.data)
container = self.get_object()
serializer = ContainerSequenceSwapSerializer(container, data=request.data, context={'container': container})
if serializer.is_valid(raise_exception=True):
# display data here
content = serializer.data
return Response(data=content, status=status.HTTP_200_OK)
Serializer
class ContainerSequenceSwapSerializer(serializers.ModelSerializer):
"""Serializer for Container sequence swap detail action."""
display_sequence = serializers.IntegerField()
# add class meta here to display id, swap sequence
class Meta:
model = Container
fields = (
"id",
"display_sequence",
)
read_only_fields = ("id", "display_sequence")
# sorting data
def to_representation(self, instance):
instance = Container.objects.filter(id__in=[self.container_id, self.target_container_id])
# data = super().to_representation(instance)
return instance
# custom validation
def validate(self, attrs):
display_sequence = super().validate(attrs)
container = self.context.get("container")
if not container:
return display_sequence
target_display_sequence = display_sequence["display_sequence"]
try:
target_container = Container.objects.get(
module=container.module, display_sequence=target_display_sequence
)
except Container.DoesNotExist:
raise serializers.ValidationError(
{"display_sequence": ["Invalid swap target"]}
)
else:
# switching
container.display_sequence, target_container.display_sequence = (
target_container.display_sequence,
container.display_sequence,
)
container.save()
target_container.save()
# datas
self.container_id = container.id
self.target_container_id = target_container.id
return display_sequence
how do I can return the expected output without modifying the views.py
I am testing an API of Django (DRF) application.
I am calling http://127.0.0.1:8000/api/users/1/documents/ (1 - user id)
And receive an error
...
File "/app/backend/apps/users/views/users/views.py" in create
542. serializer = self.get_serializer(data=request.data)
File "/usr/local/lib/python3.7/site-packages/rest_framework/generics.py" in get_serializer
110. return serializer_class(*args, **kwargs)
Exception Type: TypeError at /api/users/1/documents/
Exception Value: 'NoneType' object is not callable
How can i identify the problem?
Request related view /app/backend/apps/users/views/users/views.py (problematic line is serializer = self.get_serializer(data=request.data))
class UserDocumentCreate(generics.CreateAPIView, generics.RetrieveAPIView):
serializer_class = UserDocumentSerializer
permission_classes = (UserIsOwner, IsAuthenticatedDriver)
queryset = Document.objects.all()
def get_serializer_class(self):
if self.request.version == "1.0":
return UserDocumentSerializer
# return UserDocumentSerializer2
def create(self, request, *args, **kwargs):
request.data._mutable = True
request.data["owner"] = kwargs.get("pk")
serializer = self.get_serializer(data=request.data)
if serializer.is_valid(raise_exception=True):
owner = serializer.validated_data.get("owner")
document_type = serializer.validated_data.get("document_type")
message_status = request.data.get("message_status")
documents = owner.document_owner.filter(
document_type=document_type
)
for document in documents:
if document.status == DocumentStatus.DOCUMENT_REJECTED_STATUS:
document.delete()
# Mark user as new
owner.is_new_user = True
owner.save()
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
response = {
"status": status.HTTP_201_CREATED,
"result": serializer.data,
}
# accept corresponding registration message
if message_status:
driver_reg = DriverRegistration.objects.filter(user=kwargs.get("pk")).first()
driver_reg.accept_by_status(message_status)
next_id = driver_reg.get_next()
# add information about next registration message to response
if next_id != -1:
response["next_message"] = REG_MESSAGES[next_id].to_json()
return Response(
response, status=status.HTTP_201_CREATED, headers=headers
)
Related serializer (nothing special)
class UserDocumentSerializer(serializers.ModelSerializer):
is_new_document = serializers.BooleanField(read_only=True)
class Meta:
model = Document
fields = (
"id",
"owner",
"file",
"document_type",
"uploaded_at",
"is_new_document",
)
You need must always return a value from get_serializer_class method.
You need to implement an else condition, either explicitly (like the lone you've commented), or using the serializer_class, or falling back to super method.
class UserDocumentCreate(generics.CreateAPIView, generics.RetrieveAPIView):
serializer_class = UserDocumentSerializer
permission_classes = (UserIsOwner, IsAuthenticatedDriver)
queryset = Document.objects.all()
def get_serializer_class(self):
if self.request.version == "1.0":
return UserDocumentSerializer
else:
# explicit
return UserDocumentSerializer2
# property
return self.serializer_class
# super
return super(UserDocumentCreate, self).get_serializer_class()
I am trying to upload a csv file and then using it to populate a table in the database (creating multiple objects).
serializers.py:
def instantiate_batch_objects(data_list, user):
return [
WorkData(
work=db_obj['work'],
recordTime=db_obj['recordTime'],
user=user
) for db_obj in data_list
]
class FileUploadSerializer(serializers.ModelSerializer):
filedata = serializers.FileField(write_only=True)
class Meta:
model = WorkData
fields = ['user', 'filedata']
def create(self, validated_data):
file = validated_data.pop('filedata')
data_list = csv_file_parser(file)
batch = instantiate_batch_objects(data_list, validated_data['user'])
work_data_objects = WorkData.objects.bulk_create(batch)
# print(work_data_objects[0])
return work_data_objects
views.py:
class FileUploadView(generics.CreateAPIView):
queryset = WorkData.objects.all()
permission_classes = [IsAuthenticated]
serializer_class = FileUploadSerializer
# I guess, this is not need for my case.
def get_serializer(self, *args, **kwargs):
print(kwargs.get('data'))
if isinstance(kwargs.get('data', {}), list):
kwargs['many'] = True
return super().get_serializer(*args, **kwargs)
models.py
class WorkData(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='work_data',
)
work = models.IntegerField(blank=False, null=False)
recordTime = models.DateTimeField(blank=False, null=True)
When I upload the file and post it I get this error:
Got AttributeError when attempting to get a value for field user on serializer FileUploadSerializer. The serializer field might be named incorrectly and not match any attribute or key on the list instance. Original exception text was: 'list' object has no attribute 'user'.
But I can see table is populated successfully in the database. What should I return from create method of FileUploadSerializer?
OK, after trying an example myself I was able to reproduce the errors, I have a better understanding of why this is happing now.
First, let's put the implementation of create() on the view class here
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
The original error of Got AttributeError when attempting to get a value for field user... etc happened because the create() in the FileUploadView is returning serializer.data which is expecting fields user and filedata but create() on FileUploadSerializer is returning a list of objects so you can see now why this is happening.
You can solve this by overriding create() on FileUploadView and serialize the returned serializer data with a WorkDataSerializer that you will create
For ex:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
workData = WorkDataSerializer(data=serializer.data)
return Response(workData.data, status=status.HTTP_201_CREATED, headers=headers)
OR, you can do it on serializer level - which I prefer -
For example:
class FileUploadSerializer(serializers.ModelSerializer):
filedata = serializers.FileField(write_only=True)
created_objects_from_file = serializers.SerializerMethodField()
def get_created_objects_from_file(self, obj):
file = self.validated_data.pop('filedata')
data_list = csv_file_parser(file)
batch = instantiate_batch_objects(data_list, self.validated_data['user'])
work_data_objects = WorkData.objects.bulk_create(batch)
return WorkDataSerializer(work_data_objects, many = True).data
class Meta:
model = WorkData
fields = ['user', 'filedata']
class WorkDataSerializer(serializers.Serializer):
# fields of WorkData model you want to return
This should work with no problems, note that SerializerMethodField is read_only by default
see https://www.django-rest-framework.org/api-guide/fields/#serializermethodfield
I am new to DRF.
While creating the twitter app I faced a problem with serializers. Since the user is required - I need somehow to pass the actual user to the TweetSerializer class. I have tried different methods that did not work.
This is giving me an error
owner = serializers.HiddenField(default=serializers.CurrentUserDefault())
error image
error image continued
also i have tried to se the user by passing it tot he serializer constructor
serializer = TweetSerializer(owner=request.user)
also did not work
class TweetSerializer(serializers.ModelSerializer):
try:
owner = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
except Exception as exception:
print(exception)
class Meta:
model = Tweet
fields = '__all__'
read_only_fields = ['owner']
def validate(self, attrs):
if len(attrs['content']) > MAX_TWEET_LENGTH:
raise serializers.ValidationError("This tweet is too long")
return attrs
class Tweet(models.Model):
id = models.AutoField(primary_key=True)
owner = models.ForeignKey('auth.User', related_name='tweets', on_delete=models.CASCADE)
content = models.TextField(blank=True, null=True)
image = models.FileField(upload_to='images/', blank=True, null=True)
class Meta:
ordering = ['-id']
#api_view(['POST'])
def tweet_create_view(request, *args, **kwargs):
serializer = TweetSerializer(data=request.POST)
user = request.user
if serializer.is_valid():
serializer.save()
else:
print(serializer.errors)
return JsonResponse({}, status=400)
try:
pass
except Exception as e:
print(str(e))
return JsonResponse({}, status=400, safe=False)
return JsonResponse({}, status=201)
The solution was to pass a context as I read from drf documentation for CurrentUserDefault()
#api_view(['POST'])
def tweet_create_view(request, *args, **kwargs):
context = {
"request" : request
}
serializer = TweetSerializer(data=request.POST, context=context)
if serializer.is_valid():
serializer.save()
return JsonResponse({}, status=201)
A default class that can be used to represent the current user. In order to use this, the 'request' must have been provided as part of the context dictionary when instantiating the serializer.
Suppose I have a view and I need to check that a field is given before calling serializer.save to ensure I don't get a dictionary key error:
class BarView(CreateAPIView):
serializer_class = BarSerializer
queryset = Bar.objects.all()
def perform_create(self, serializer):
if 'foo' not in self.request.data:
raise ParseError('foo field required.')
foo = get_object_or_404(Foo, pk=self.request.data['foo'])
if foo.counter == 10:
raise ParseError('foo limit reached.')
return serializer.save(user=self.request.user, foo=foo)
Instead of returning "foo field required." I would like to return a message the same as Django REST returns e.g. {"foo":["This field is required."]}
Is there a better way to do this? Perhaps validating the foo field alone with the serializer?
Update: I forgot to mention the user field is also required.
The model for Bar is:
class Bar(models.Model):
user = models.ForeignKey(User, db_index=True, editable=False)
foo = models.ForeignKey(Foo, db_index=True)
Yes,
Simply look at docs: Validation
(I assumed that field foo is part of Bar model, if not please add it to fields in Meta):
Add validation to BarSerializer:
class BarSerializer(serializers.ModelSerializer):
def validate_foo(self, value):
if not value:
raise serializers.ValidationError("foo field required.")
if Foo.objects.filter(pk=value, counter__gte=10).exists():
raise serializers.ValidationError("foo limit reached.")
return value
class Meta:
model = Bar
And then create Your View by extending this:
from rest_framework.exceptions import ValidationError
class MyCreateAPIView(CreateAPIView):
def post(self, request, *args, **kwargs):
try:
return super(BarView, self).post(request, *args, **kwargs)
except ValidationError as e:
return Response(e.detail, , status=status.HTTP_400_BAD_REQUEST)
def create(self,request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
self.perform_create(serializer)
except DjangoValidationError as e:
raise ValidationError(e.messages)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(serializer):
# do your stuff
serializer.save()
Yes, the best way to do this is to make the foo field required in your serializer using extra_kwargs option in the Meta class.
DRF will automatically handle the validation for you. You don't need to raise this validation error yourself.
class Meta:
...
extra_kwargs = {'foo': {'required':True}} # make 'foo' a required field.
Now, whenever the foo field is not passed in the request, there will be a key foo in the serializer.errors dictionary and its value will be This field is required.
Also, create a validate_foo() function which will validate for the limit of foo_object.counter.
def validate_foo(self, value):
self.foo_object = get_object_or_404(Foo, pk=value) # get the 'foo' object
if self.foo_object.counter == 10: # check for limits
raise serializers.ValidationError('foo limit reached.') # raise error
return value # must return the value at the end
FINAL CODE:
serializers.py
class BarSerializer(serializers.ModelSerializer):
class Meta:
...
extra_kwargs = {'foo': {'required':True}} # make 'foo' a required field.
def validate_foo(self, value):
self.foo_object = get_object_or_404(Foo, pk=value)
if self.foo_object.counter == 10:
raise serializers.ValidationError('foo limit reached.')
return value
views.py
In your views, you need to override perform_create() and pass user and serializer.foo_object to serializer.save() function.
class BarView(CreateAPIView):
serializer_class = BarSerializer
queryset = Bar.objects.all()
def perform_create(self, serializer):
return serializer.save(user=self.request.user, foo=serializer.foo_object)
You can return Response with user defined message
if 'foo' not in self.request.data:
return Response({"foo":["This field is required."]})