How to deserialize POST request using a different Serializer - django

I'm trying to handle POST requests in my API endpoint using a different serializer than the one derived from Model and used for GET or PUT requests. The format of the POSTed messages is different from the Model and from GET/PUT and must be pre-processed before storing to the database.
As a demo of my problem I made a very simple model and the corresponding API view and serializer:
class Message(models.Model):
message = models.CharField(max_length = 500)
class MessageSerializer(serializers.ModelSerializer):
class Meta:
model = Message
fields = ('message',)
class MessageViewSet(viewsets.ModelViewSet):
queryset = Message.objects.all().order_by('-pk')
serializer_class = MessageSerializer
That works well. Then I tried to override the MessageViewSet.create() to handle POST requests differently.
class MessageSerializer_FromTo(serializers.Serializer):
sender = serializers.EmailField()
recipient = serializers.EmailField()
def create(self, validated_data):
message = "Message from <{sender}> to <{recipient}>".format(**validated_data)
return Message(message)
class MessageViewSet(viewsets.ModelViewSet):
queryset = Message.objects.all().order_by('-pk')
serializer_class = MessageSerializer
# Handle POST requests differently
def create(self, request, format=None):
message = MessageSerializer_FromTo(data = request.data)
if message.is_valid():
message.save()
return Response(message.data, status=status.HTTP_201_CREATED)
return Response(message.errors, status=status.HTTP_400_BAD_REQUEST)
Essentially I want to pass this JSON to POST /api/messages/
{"sender": "some#one.com", "recipient": "someone#else.org"}
GET /api/messages/1/ should return
{"message": "Message from <some#one.com> to <someone#else.org>"}
However the POST fails with this message:
Internal Server Error: /api/messages/
Traceback (most recent call last):
File ".../rest_framework/fields.py", line 441, in get_attribute
return get_attribute(instance, self.source_attrs)
File ".../rest_framework/fields.py", line 100, in get_attribute
instance = getattr(instance, attr)
AttributeError: 'Message' object has no attribute 'sender'
During handling of the above exception, another exception occurred:
[...]
AttributeError: Got AttributeError when attempting to get a value for field `sender` on serializer `MessageSerializer_FromTo`.
The serializer field might be named incorrectly and not match any attribute or key on the `Message` instance.
Original exception text was: 'Message' object has no attribute 'sender'.
[26/Feb/2018 05:09:08] "POST /api/messages/ HTTP/1.1" 500 19059
This is just to demonstrate the problem obviously, I'm doing more complex things in my POST handler, but the error is like this.
Any idea how to achieve what I need? I.e. accept POST fields that are completely different from the Model fields?
Thanks!
UPDATE: The complete code is here: https://github.com/mludvig/drf-demo

problem is with in your serializer. You are just passing model class Message(message) as the output of create function instead of Message object
class MessageSerializer_FromTo(serializers.Serializer):
sender = serializers.EmailField(write_only=True)
recipient = serializers.EmailField(write_only=True)
message = serializers.CharField(read_only=True, max_length = 500)
def create(self, validated_data):
message = "Message from <{sender}> to <{recipient}>".format(**validated_data)
return Message.objects.create(message=message)

To use different serializers depending on if the serializer will deserialize the data from the request or serialize it for the response, you can use drf-rw-serializers.
It has generic classes to help you build views like the Django REST Framework ones, but, instead of using only one serializer_class, they use a read_serializer_class and a write_serializer_class.
You can read more instruction about installation and usage in the documentation.

You can get a different serializer depending on the method type by overriding the get_serializer_class method of you viewset
class MessageViewSet(viewsets.ModelViewSet):
model = Message
def get_serializer_class(self):
serializers_class_map = {
'default': DefaultSerializer,
'create': CreateMessageSerializer,
}
return serializers_class_map.get(self.action, serializers_class_map['default'])
You can also overrides the to_representation method of your CreateMessageSerializer to get a different output
class CreateMessageSerializer(serializer.ModelSerializer):
class Meta:
model = Message
fields = (
'sender', 'recipient'
)
def to_representation(self, instance):
return {
'message': "Message from <{0.sender}> to <{0.recipient}>".format(instance)
}

Related

Django rest framework - Serializer always returns empty object ({})

I am creating first rest api in django using django rest framework
I am unable to get object in json format. Serializer always returns empty object {}
models.py
class Shop(models.Model):
shop_id = models.PositiveIntegerField(unique=True)
name = models.CharField(max_length=1000)
address = models.CharField(max_length=4000)
serializers.py
class ShopSerializer(serializers.ModelSerializer):
class Meta:
model = Shop
fields = '__all__'
views.py
#api_view(['GET'])
def auth(request):
username = request.data['username']
password = request.data['password']
statusCode = status.HTTP_200_OK
try:
user = authenticate(username=username, password=password)
if user:
if user.is_active:
context_data = request.data
shop = model_to_dict(Shop.objects.get(retailer_id = username))
shop_serializer = ShopSerializer(data=shop)
if shop:
try:
if shop_serializer.is_valid():
print('is valid')
print(shop_serializer.data)
context_data = shop_serializer.data
else:
print('is invalid')
print(shop_serializer.errors)
except Exception as e:
print(e)
else:
print('false')
else:
pass
else:
context_data = {
"Error": {
"status": 401,
"message": "Invalid credentials",
}
}
statusCode = status.HTTP_401_UNAUTHORIZED
except Exception as e:
pass
return Response(context_data, status=statusCode)
When i try to print print(shop_data) it always returns empty object
Any help, why object is empty rather than returning Shop object in json format?
Edited:
I have updated the code with below suggestions mentioned. But now, when shop_serializer.is_valid() is executed i get below error
{'shop_id': [ErrorDetail(string='shop with this shop shop_id already exists.', code='unique')]}
With the error it seems it is trying to update the record but it should only get the record and serialize it into json.
You're using a standard Serializer class in this code fragment:
class ShopSerializer(serializers.Serializer):
class Meta:
model = Shop
fields = '__all__'
This class won't read the contend of the Meta subclass and won't populate itself with fields matching the model class. You probably meant to use ModelSerializer instead.
If you really want to use the Serializer class here, you need to populate it with correct fields on your own.
.data - Only available after calling is_valid(), Try to check if serializer is valid than take it's data

Serializer in patch method does not check fields

I am trying to partially update a model and overwriting the patch method. It works fine when I send correct data, but my problem is, that when I send incorrect data the serializer stays True and I still get a 200 status code and nothing gets updated. With incorrect data I mean wrong field names. I could basically send any field name and get a 200 status code. So I think the fields are not checked by the serializer...
class BuildingUpdateAPI(UpdateAPIView):
serializer_class = BuildingSerializer
def patch(self, request, *args, **kwargs):
"""Patches building."""
building = buildings.get(name=self.kwargs['name_building'])
serializer = BuildingSerializer(building, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return JsonResponse(status=200, data=serializer.data)
return JsonResponse(status=400, data="Wrong data provided")
serializer:
class BuildingSerializer(serializers.ModelSerializer):
"""Serializer for Buildings."""
class Meta:
model = Building
fields = (
"id",
"name",
"...",
.....
Now I am wondering if this is like this by design or if I am doing something wrong in my view. I thought I could overwrite the validate-method in the serializer but my model has quite a lot of fields... So I would have to check every field single field which is not optimal I think. Or is there a way to do this somehow elegantly?
I am patching like this:
result = requests.request("patch", url=endpoint, json=payload, headers=get_headers())

DRF SerializerMethodField method only running when serializer variable opened in debugger

My SerializerMethodField method is only printing HERE when I have a breakpoint at the return in the get method, and open the serializer variable after it has triggered OR when serializer.data is called (in which case it prints the expected data, but validated_data is still empty).
View:
class EventAddPeople(generics.GenericAPIView):
serializer_class = EventAddPeopleSerializer_Read
def get(self, request, *args, **kwargs):
serializer = EventAddPeopleSerializer_Read(data=request.GET)
serializer.is_valid(raise_exception=True)
print(serializer.validated_data)
return HttpResponse(serializer.validated_data)
Serializer:
class EventAddPeopleSerializer_Read(serializers.Serializer):
event_id = serializers.SerializerMethodField(method_name='get_event_id')
person_ids = serializers.SerializerMethodField()
def get_event_id(self, obj):
print("HERE")
return "TEST00"
def get_person_ids(self, obj):
print("HERE")
return "TEST00"
class Meta:
fields = ('event_id', 'person_ids')
Your get method is not called (may be).
check by just printing('anything') in your get method
check Methos for genericApiViews
Thanks
First thing, request.data is applicable for non-GET requests. You are not supposed to send data in the payload section with HTTP GET. If you want to send data with GET method, pass it through URL query parameters
So, the url will become, /api/my/end-point/?event_id=1&person_ids=3
and you need to pass this query param to serializer as,
serializer = EventAddPeopleSerializer_Read(data=request.GET)
Second thing, you've missed to add the Meta class in the serializer
class EventAddPeopleSerializer_Read(serializers.Serializer):
# other code
class Meta:
fields = ('event_id', 'person_ids')

How to Create a Django Model Instance That Contains a Required OneToOneField with DRF Serializers

The Question
Let's say I have a model that contains a OneToOneField like so:
models.py
class Event(models.Model)
# some basic fields...
class EventDetail(models.Model):
event = models.OneToOneField(Event, on_delete=models.CASCADE,
related_name='event_detail')
# other basic fields, all with default values...
What is a proper way to implement a POST request that intends to create a new Event in the database that automatically creates a default EventDetail linked to it if it is None in the request header, using Django Rest Framework's Serializers?
My Attempt
test.py
class EventTestCase(APITestCase):
def test_post(self):
# Need to provide an id, or else an error occurs about the OneToOneField
event = Event(id=1)
serializer = serializers.EventSerializer(event)
res = self.api_client.post('/event/', serializer.data)
views.py
def post(self, request, format=None):
serializer = EventSerializer(
data=request.data)
# ***This always returns false!***
if serializer.is_valid():
try:
instance = serializer.save()
except ValueError:
return Response(status=status.HTTP_400_BAD_REQUEST)
serializers.py
class EventSerializer(serializers.ModelSerializer):
serialization_title = "Event"
event_detail = EventDetailSerializer()
class Meta:
model = Event
exclude = ('id',)
error_status_codes = {
HTTP_400_BAD_REQUEST: 'Bad Request'
}
class EventDetailSerializer(serializers.ModelSerializer):
serialization_title = "Event Detail"
class Meta:
model = models.EventDetail
exclude = ('id',)
error_status_codes = {
HTTP_400_BAD_REQUEST: 'Bad Request'
}
As noted in a comment above, serializer.is_valid() always returns false with the error:
{'event_detail': [u'This field may not be null.']}
I understand that this is complaining because both EventDetail and Event need to be created in order for a OneToOne relationship to be added, but how do you deal with this using Serializers when a OneToOneField is required and yet not provided by the client?
Thank you for your help.
Disclaimer: I am using Django 1.11
You can declare the EventDetailSerializer with read_only=True or required=False and then handle the creation of the EventDetail in different ways, for example: you could have a post_save signal that listens to the Event class - once a new Event object has been created, you can then create the initial EventDetail object, or you perform this creation after your serializer.save() on the post definition, or even on your create method of your EventSerializer.
edit: an example on how you could perform the creation using the EventDetailSerializer and overriding the create method of your EventSerializer.
def create(self, validated_data):
detail = self.initial_data.get('event_detail')
instance = super().create(validated_data)
if detail:
serializer = EventDetailSerializer(data=detail)
serializer.is_valid(raise_exception=True)
serializer.save(event=instance)
return instance

Django Query from Post

I am trying to pass into my DRF API a post that needs to be queried against the database. For now I have included only 2 fields that can be queried. I am having trouble getting the view to work. Here is what I have so far.
POST
{
"city": "Denver",
"state": "CO"
}
Serializer only lets city and state be accepted
class EventQuerySerializer(serializers.ModelSerializer):
class Meta:
model = Events
fields = ('city', 'state')
View note that EventsSerializer is used in the return and I can confirm it works for a basic get request
class QueryEvents(APIView):
#staticmethod
def post(request):
serializer = EventQuerySerializer(data=request.data)
if serializer.is_valid():
events = Events.objects.get(serializer)
return Response(EventsSerializer(events).data)
Error
AttributeError: 'CharField' object has no attribute 'split'
You need to run save on you serializer to retrieve instance:
# ...
if serializer.is_valid():
event = serializer.save()
events = Events.objects.get(pk=event.pk)
# ...