How do I throw the serializer.errors if I have a wrong input in some field? Do I have to code some "logic", or I just have to write some "configurations".
#models.py
class Product(models.Model):
name = models.CharField()
amount = models.IntegerField()
description = models.TextField()
#serializers.py
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = '__all__'
def create(self, validated_data):
...
def update(self, instance, validated_data):
...
#views.py
class ProductViewSet(viewsets.ModelViewSet):
serializer_class = ProductSerializer
def partial_update(self, request, pk=None):
...
def get_queryset(self):
...
For example I want to POST:
{
"name": "Banana",
"amount": "ABCD",
"description": ""
}
Instead of getting an error in the server:
ValueError: invalid literal for int() with base 10: 'ABCD'
I want a response like this:
{"amount": ["A valid integer is required."], "description": ["This field may not be blank."]}
As far the model is designed, the modelserializer will take care. In case you want more control of validation in your hand then you can do it in your serializers. For your problem, the following snippets would work.
#serializers.py
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = '__all__'
def create(self, validated_data):
...
def update(self, instance, validated_data):
...
def validate(self, attrs):
# you can add your validation rule here
if not isinstance(attrs.get('amount'), int):
serializers.ValidationError({
"amount": ["A valid integer is required."],
"description": ["This field may not be blank."]
})
In this way, you can use validate() to validate any field of your serializer as you wish. Official documentation on validator in serializer, Also you can use validate_<field_name>(self, <field_name>) function to validate a specific field of a serializer. Like mentioned in this answer.
Related
I'm using the djangorestframework module to set up an API to update/read my models. I have these models ...
from django.db import models
from address.models import AddressField
from phonenumber_field.modelfields import PhoneNumberField
from address.models import State
from address.models import Country
class CoopTypeManager(models.Manager):
def get_by_natural_key(self, name):
return self.get_or_create(name=name)[0]
class CoopType(models.Model):
name = models.CharField(max_length=200, null=False)
objects = CoopTypeManager()
class Meta:
unique_together = ("name",)
class Coop(models.Model):
name = models.CharField(max_length=250, null=False)
type = models.ForeignKey(CoopType, on_delete=None)
address = AddressField(on_delete=models.CASCADE)
enabled = models.BooleanField(default=True, null=False)
phone = PhoneNumberField(null=True)
email = models.EmailField(null=True)
web_site = models.TextField()
and I have these view classes ...
class CoopList(APIView):
"""
List all coops, or create a new coop.
"""
def get(self, request, format=None):
coops = Coop.objects.all()
serializer = CoopSerializer(coops, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = CoopSerializer(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)
class CoopDetail(APIView):
"""
Retrieve, update or delete a coop instance.
"""
def get_object(self, pk):
try:
return Coop.objects.get(pk=pk)
raise Http404
def get(self, request, pk, format=None):
coop = self.get_object(pk)
serializer = CoopSerializer(coop)
return Response(serializer.data)
def put(self, request, pk, format=None):
coop = self.get_object(pk)
serializer = CoopSerializer(coop, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, format=None):
coop = self.get_object(pk)
coop.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Problem is, I would like to submit JSON like this
{
"name": "1872",
"type": {
"name": "Coworking Space"
},
with the intention being that for the dependent CoopType model, I would either create one of use an existing one before creating my Coop model. However, right now, submitting the above results in a 400 response ...
{"type":["Incorrect type. Expected pk value, received dict."]
How should I modify my view class to accommodate what I'm trying to do?
Edit: the serializers ...
from rest_framework import serializers
from maps.models import Coop, CoopType
from address.models import Address, AddressField, Locality, State, Country
class CoopSerializer(serializers.ModelSerializer):
class Meta:
model = Coop
fields = ['id', 'name', 'type', 'address', 'enabled', 'phone', 'email', 'web_site']
def to_representation(self, instance):
rep = super().to_representation(instance)
rep['type'] = CoopTypeSerializer(instance.type).data
rep['address'] = AddressSerializer(instance.address).data
return rep
def create(self, validated_data):
"""
Create and return a new `Snippet` instance, given the validated data.
"""
return Coop.objects.create(**validated_data)
def update(self, instance, validated_data):
"""
Update and return an existing `Coop` instance, given the validated data.
"""
instance.name = validated_data.get('name', instance.name)
instance.type = validated_data.get('type', instance.type)
instance.address = validated_data.get('address', instance.address)
instance.enabled = validated_data.get('enabled', instance.enabled)
instance.phone = validated_data.get('phone', instance.phone)
instance.email = validated_data.get('email', instance.email)
instance.web_site = validated_data.get('web_site', instance.web_site)
instance.web_site = validated_data.get('web_site', instance.web_site)
instance.save()
return instance
class CoopTypeSerializer(serializers.ModelSerializer):
class Meta:
model = CoopType
fields = ['id', 'name']
def create(self, validated_data):
"""
Create and return a new `CoopType` instance, given the validated data.
"""
return CoopType.objects.create(**validated_data)
def update(self, instance, validated_data):
"""
Update and return an existing `CoopType` instance, given the validated data.
"""
instance.name = validated_data.get('name', instance.name)
instance.save()
return instance
Edit 2: The curl request ...
#!/bin/bash
read -d '' req << EOF
{
"name": "1872",
"type": {
"name": "Coworking Space"
},
"address": {
"id": 1,
"street_number": "222",
"route": "1212",
"raw": "222 W. Merchandise Mart Plaza, Suite 1212",
"formatted": "222 W. Merchandise Mart Plaza, Suite 1212",
"latitude": 41.88802611,
"longitude": -87.63612199,
"locality": {
"id": 29,
"name": "Chicago",
"postal_code": "60654",
"state": {
"id": 1,
"name": "IL",
"code": "",
"country": {
"id": 484,
"name": "United States",
"code": "US"
}
}
}
},
"enabled": true,
"phone": null,
"email": null,
"web_site": "http://www.1871.com/"
}
EOF
echo $req
curl -v --header "Content-type: application/json" --data "$req" --request POST "http://127.0.0.1/coops/"
This should work:
class CoopTypeField(serializers.PrimaryKeyRelatedField):
queryset = CoopType.objects
def to_internal_value(self, data):
if type(data) == dict:
cooptype, created = CoopType.objects.get_or_create(**data)
# Replace the dict with the ID of the newly obtained object
data = cooptype.pk
return super().to_internal_value(data)
class CoopSerializer(serializers.ModelSerializer):
type = CoopTypeField()
# ... the rest of this class is unchanged
Where the changes are:
Define a custom CoopTypeField(). The to_internal_value() method would ordinarily be expecting an ID - so we override it to accept data in the form of a dictionary and convert this to an ID (by getting or creating a CoopType) and then pass this ID to the parent class method.
Define a type on the CoopSerializer that uses this new CoopTypeField().
Now your CoopSerializer will accept data in two forms:
{'name': 'Coop 1', 'web_site': 'http://example.com', 'type': {'name': 'Coop Type 1'}}
or
{'name': 'Coop 1', 'web_site': 'http://example.com', 'type': 1}
(I've omitted the other fields that Coop requires for brevity).
Based on the current serializer, the CoopType needs to be made ahead of time (or fetched ahead of time if it exists) so that the id can be passed along. If you want to still make the CoopType with nested data in the Coop serializer (or use it to find the CoopType to use), Django Rest Framework talks about how in their docs under Writable nested serializers.
Oh boy... There is so much we could improve upon here. So let's start with your question first.
#tredzko(https://stackoverflow.com/a/60310659/3627387) was right and you should look into https://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers
By the look of your CoopType model I see that name field is unique.
from rest_framework import serializers
from maps.models import Coop, CoopType
from address.models import Address, AddressField, Locality, State, Country
class CoopTypeSerializer(serializers.ModelSerializer):
class Meta:
model = CoopType
fields = ['id', 'name']
class CoopSerializer(serializers.ModelSerializer):
# type field should be defined here instead of `to_representation`
type = CoopTypeSerializer()
class Meta:
model = Coop
fields = ['id', 'name', 'type', 'address', 'enabled', 'phone', 'email', 'web_site']
def to_representation(self, instance):
# this is the correct way of extending to_representation
# we set update self.fields and after that
# Serializer class handles everything automatically
self.fields['address'] = AddressSerializer(read_only=True)
return super().to_representation(instance)
def validate_type(self, value):
coop_type, __ = CoopType.objects.get_or_create(**coop_type_data)
return coop_type
Something along this lines should work.
Now let's dive into other improvements
I suggest using ModelViewSet(https://www.django-rest-framework.org/api-guide/viewsets/#modelviewset)
So you could do this(see below) instead of your views code
from rest_framework import viewsets
class CoopViewSet(viewsets.ModelViewSet):
serializer_class = CoopSerializer
queryset = Coop.objects.all().select_related('type')
Your CoopType model is defined in an odd way, I think you wanted to actually do this.
class CoopType(models.Model):
# https://docs.djangoproject.com/en/3.0/ref/models/fields/#unique
name = models.CharField(max_length=200, null=False, unique=True)
class Meta:
pass
I have two models as:
class Book(AppModel):
title = models.CharField(max_length=255)
class Link(AppModel):
link = models.CharField(max_length=255)
class Page(AppModel):
book= models.ForeignKey("Book", related_name="pages", on_delete=models.CASCADE)
link = models.ForeignKey("Link", related_name="pages", on_delete=models.CASCADE)
page_no = models.IntegerField()
text = models.TextField()
and serializers
class LinkSerializer(serializers.ModelSerializer):
class Meta:
model = Link
fields = ['link']
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = ('link','text','page_no')
def validate_text(self, value):
#some validation is done here.
def validate_link(self, value):
#some validation is done here.
class BookSerializer(serializers.ModelSerializer):
pages = PageSerializer(many=True)
class Meta:
model = Book
fields = ('title','pages')
#transaction.atomic
def create(self, validated_data):
pages_data= validated_data.pop('pages')
book = self.Meta.model.objects.create(**validated_data)
for page_data in pages_data:
Page.objects.create(book=book, **page_data)
return book
There is a validate_text method in PageSerializer. The create method will never call the PageSerializer and the page_data is never validated.
So I tried another approach as:
#transaction.atomic
def create(self, validated_data):
pages_data = validated_data.pop('pages')
book = self.Meta.model.objects.create(**validated_data)
for page_data in pages_data:
page = Page(book=book)
page_serializer = PageSerializer(page, data = page_data)
if page_serializer.is_valid():
page_serializer.save()
else:
raise serializers.ValidationError(page_serializer.errors)
return book
Posted data:
{
"title": "Book Title",
"pages": [
{
"link": 1, "page_no": 52, "text": "sometext"
}
]
}
But the above approach throws error:
{
"link": [
"Incorrect type. Expected pk value, received Link."
]
}
I also found why this error is caused: Though I am posting data with pk value 1 of a Link, the data when passed to the PageSerializer from the BookSerializer appears as such: {"link": "/go_to_link/", "page_no":52, "text": "sometext"}
Why is an instance of Link passed to the PageSerializer whereas what I sent is pk of Link?
To validate a nested object using a nested serializer:
#transaction.atomic
def create(self, validated_data):
pages_data = validated_data.pop('pages') #pages data of a book
book= self.Meta.model.objects.create(**validated_data)
for page_data in pages_data:
page = Page(book=book)
page_serializer = PageSerializer(page, data = page_data)
if page_serializer.is_valid(): #PageSerializer does the validation
page_serializer.save()
else:
raise serializers.ValidationError(page_serializer.errors) #throws errors if any
return book
Suppose you send the data as:
{
"title": "Book Title",
"pages": [{
"link":2,#<= this one here
"page_no":52,
"text":"sometext"}]
}
In the above data we are sending an id of the Link object. But in the create method of the BookSerializer defined above, the data we sent changes to:
{
"title": "Book Title",
"pages": [{
"link":Link Object (2),#<= changed to the Link object with id 2
"page_no":52,
"text":"sometext"}]
}
And the PageSerializer is actually meant to receive an pk value of the link i.e, "link": 2 instead of "link":Link Object (2). Hence it throws error:
{
"link": [
"Incorrect type. Expected pk value, received Link."
]
}
So the workaround is to override the to_internal_value method of the nested serializer to convert the received Link Object (2) object to its pk value.
So your PageSerializer class should then be:
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = ('link','text','page_no')
def to_internal_value(self, data):
link_data = data.get("link")
if isinstance(link_data, Link): #if object is received
data["link"] = link_data.pk # change to its pk value
obj = super(PageSerializer, self).to_internal_value(data)
return obj
def validate_text(self, value):
#some validation is done here.
def validate_link(self, value):
#some validation is done here.
and the parent serializer:
class BookSerializer(serializers.ModelSerializer):
pages = PageSerializer(many=True)
class Meta:
model = Book
fields = ('title','pages')
#transaction.atomic
def create(self, validated_data):
pages_data = validated_data.pop('pages')#pages data of a book
book= self.Meta.model.objects.create(**validated_data)
for page_data in pages_data:
page = Page(book=book)
page_serializer = PageSerializer(page, data = page_data)
if page_serializer.is_valid(): #PageSerializer does the validation
page_serializer.save()
else:
raise serializers.ValidationError(page_serializer.errors) #throws errors if any
return book
That should allow the nested serializer to do the validation instead of writing validation inside the create method of the parent serializer and violating DRY.
When you call serializer.is_valid(raise_exception=True/False) it automatically call validate functions of nested serializer. When you call serializer.save(**kwargs) serializer passes validated data into your create(self, validated_data) or update(self, instance, validated_data) functions of serializer. Moreover, in validated data your ForeignKey fields returned an object.
def create(self, validated_data):
pages_data = validated_data.pop('pages') # [{'link': Linkobject, ...}]
book= self.Meta.model.objects.create(**validated_data)
for page_data in pages_data:
page = Page(book=book)
page_serializer = PageSerializer(page, data = page_data) # here you are sending object to validation again
if page_serializer.is_valid():
page_serializer.save()
else:
raise serializers.ValidationError(page_serializer.errors)
return book
I would like to update certain properties of a user (say first_name and last_name)
my json object through a PUT request would look like this
{
"user" : {
"first_name": "Jack",
"last_name": "shnider",
"password":"admin123"
"email" : "foo#google.com"
},
"employee_zip" : 12345
}
This is what my view looks like (I would like to update the existing fields to these new fields).
These are the serializer
class Serializer_UpdateUser(ModelSerializer):
class Meta:
model = User
fields = ('first_name','last_name','password')
class Serializer_UpdateEmployer(ModelSerializer):
user = Serializer_UpdateUser()
class Meta:
model = modelEmployer
fields = [
'user',
'employer_zip',
]
This is the view :
class UpdateProfile_RetrieveUpdateAPIView(RetrieveUpdateAPIView):
queryset = modelEmployer.objects.all()
serializer_class = Serializer_UpdateEmployer
lookup_field = 'user__email'
permission_classes = [permissions.AllowAny]
def update(self, request, *args, **kwargs):
instance = self.get_object() #------>I have the object that I would like to update
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True) #--->Success
Now I would like to get a validated fields (The json only contains the fields that have been updated). I know if I do something like this
serializer.save
I would get back a modelEmployer but instead I get back this error
AssertionError at /api/employer/update_profile/employerA#gmail.com/ The `.update()` method does not support writable nested fields by default. Write an explicit `.update()` method for serializer `Employer.api.serializers.Serializer_ListEmployer`, or set `read_only=True` on nested serializer fields. Request Method:
I have two questions
1-Why is save failing ?
2-How can I get the validated data from the above serializer ?
The save is failing because django-rest-framework doesn't deal with nested serializers by default.
from the django-rest-framework docs:
By default nested serializers are
read-only. If you want to support write-operations to a nested
serializer field you'll need to create create() and/or update()
methods in order to explicitly specify how the child relationships
should be saved.
You have to override the update method in the serializer to allow that behavior:
class Serializer_UpdateEmployer(ModelSerializer):
user = Serializer_UpdateUser()
class Meta:
model = modelEmployer
fields = [
'user',
'employer_zip',
]
def update(self, instance, validated_data):
user_data = validated_data.pop('user', {})
user_serializer = Serializer_UpdateUser(instance.user, data=user_data)
user_serializer.save()
return instance
Another solution is to use drf-writable-nested. It automatically makes your nested serializers updatable.
from drf_writable_nested import WritableNestedModelSerializer
class Serializer_UpdateEmployer(WritableNestedModelSerializer):
user = Serializer_UpdateUser()
class Meta:
model = modelEmployer
fields = [
'user',
'employer_zip',
]
I think drf-writable-nested can help you to update nested data.
In you case:
from django.contrib.auth import password_validation
class Serializer_UpdateUser(ModelSerializer):
def update(self, instance, validated_data):
password = validated_data.pop('password', None)
super(Serializer_UpdateUser, self).update(instance, validated_data)
if password is not None:
instance.set_password(password)
instance.save()
return instance
def validate_password(self, value):
password_validation.validate_password(value)
return value
class Meta:
model = User
fields = ('first_name','last_name','password')
class Serializer_UpdateEmployer(WritableNestedModelSerializer):
user = Serializer_UpdateUser()
class Meta:
model = modelEmployer
fields = [
'user',
'employer_zip',
]
Note you need special handling password field.
This very simple django restframework code.
models.py
class User(models.Model)
Email = models.CharField(max_length=100)
Username = models.CharField(max_length=100)
State = models.CharField(max_length=100)
serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('Email','Username','State')
views.py
class UserList(generics.ListCreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
If use this I am getting error out put like this
{
"Email": [
"This field may not be blank."
],
"Username": [
"This field may not be blank."
],
"Country": [
"This field may not be blank."
],
}
But I need to change the error out like this.How I can i archive this and any suggestion greatly appreciated.
{"error":
[
"Email is required",
"Username is required",
"County is required"
]
}
You can always overwrite the create/update methods from the generic views. It would be something like:
class UserList(generics.ListCreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if not serializer.is_valid(raise_exception=False):
# TODO: add here your custom error dict using serializer.errors
return Response({"error":...}, status=...)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
Or... you could try to overwrite the serializers... if you don't want to overwrite the view.
(However, there must be a good reason for a JS developer not being able to parse a simple json error object :P)
Hope this helps
You can define your own error messages for any error case:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('Email','Username','State')
def __init__(self, *args, **kwargs):
super(UserSerializer, self).__init__(*args, **kwargs)
for field in self.Meta.fields:
self.fields[field].error_messages['required'] = "%s is required" % field
I have UserSerializer and nested UserClientSerializer. I'm trying to update info for logged user. But I receive unique_together validation error.
I have the following models:
models.py
class UserClients(models.Model):
user = models.ForeignKey(User, related_name='user_clients')
client = models.ForeignKey(Client, related_name='client_users')
belongs_to = models.BooleanField(default=False)
for_future = models.BooleanField(default=False)
class Meta:
unique_together = ('user', 'client')
Also I have two seralizers.
serializers.py
class UserClientsSerializer(serializers.ModelSerializer):
class Meta:
model = UserClients
class UserSerializer(serializers.ModelSerializer):
user_clients = UserClientsSerializer(required=False, allow_null=True, many=True)
class Meta:
model = get_user_model()
exclude = ('password','username', 'date_joined', 'is_superuser')
#transaction.atomic
def create(self, validated_data):
...
#transaction.atomic
def update(self, instance, validated_data):
...
views.py
class CurrentUserDetails(RetrieveUpdateAPIView):
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
def get_object(self):
return self.request.user
So, when I trying to update my user data, for example "belongs_to" was False, I want to change it to False.
My my JSON data is like this.
{
"user_clients": [
{
"id": 57,
"belongs_to": true,
"for_future": false,
"user": 25,
"client": 3
}
]
}
but i receive validation error like this
{
"user_clients": [
{
"non_field_errors": [
"The fields user, client must make a unique set."
]
}
]
}
Do you have any idea about this problem?
The second post down on this link seems like it might describe a similar problem and a potential workaround that should work for you:
https://github.com/tomchristie/django-rest-framework/issues/2380
It seems like you are trying to create a new object rather than updating the old object which is why you are getting the unique together error.
If that is not the case then try turning off validation and using your own as described in the above link.