I am trying to serialise a json payload that has a field with an array, the .is_valid() check is returning true but I am getting KeyError: 'passengers' when I try to do this serializer.data['passengers'] but the other fields work fine (such as booking_number and status).
This is the response.data I am passing to the seralizer:
{'booking_number': 2839, 'passengers': [{'first_name': 'Jack', 'surname': 'Smith', 'email': 'smith#mail.com', 'phone_number': '1234'}], 'status': 'ON_HOLD'}
My seralizers:
class PassengerSerializer(serializers.ModelSerializer):
class Meta:
model = Passenger
class FindBus(serializers.ModelSerializer):
passengers = PassengerSerializer(read_only=True, many=True)
class Meta:
model = Booking
fields = ('booking_number', 'passengers', 'status')
My models:
class Passenger(models.Model):
first_name = models.CharField(max_length=25)
surname = models.CharField(max_length=25)
email = models.EmailField()
phone_number = models.CharField(max_length=12)
class Booking(models.Model):
booking_number = models.IntegerField(unique=True)
passenger = models.ManyToManyField(Passenger)
status = models.CharField(max_length=10)
hold_time = models.DateTimeField()
Any advise on how to get this working would be greatly appreciated.
Btw I was following this: Django rest framework serializing many to many field
If you need to de-serialize fields, you should not use read_only=True:
class FindBus(serializers.ModelSerializer):
passengers = PassengerSerializer(many=True)
...
Note that this won't be enough for saving m2m relationships: as explained in Writable nested serializers, you'll also need to define create() and/or update() methods on your serializer:
class FindBus(serializers.ModelSerializer):
passengers = PassengerSerializer(many=True)
...
def create(self, validated_data):
...
def update(self, validated_data):
...
The reason for the need of the create/update is that you have to decide whether the passenger details that you receive refer to existing objects or need to be created.
You might also need to add fields = ('__all__',) (or specify the fields you're interested in) to your PassengerSerializer:
class PassengerSerializer(serializers.ModelSerializer):
class Meta:
model = Passenger
fields = ('__all__',)
Related
In my app I have a nested many to many relation like the following:
Models.py
class ReturnKitsProducts(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.IntegerField(default=0)
class ReturnKits(models.Model):
kit = models.ForeignKey(Kit, on_delete=models.CASCADE)
quantity = models.IntegerField(default=0)
items = models.ManyToManyField(ReturnKitsProducts)
class Return(models.Model):
transaction_date = models.DateTimeField(default=datetime.now)
transaction_no = models.IntegerField(default=0, blank=True, null=True)
flow = models.ForeignKey(Flow, on_delete=models.CASCADE)
kits = models.ManyToManyField(ReturnKits)
warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE)
In this ReturnKitsProducts is connected to ReturnKits as M2M and ReturnKits is connected to Return as M2M. I have handles only single level of M2M serialization for updatation and creation like this:
Serializers.py
class ReturnKitsSerializer(serializers.ModelSerializer):
class Meta:
model = ReturnKits
fields = "__all__"
class ReturnSerializer(serializers.ModelSerializer):
kits = ReturnKitsSerializer(many=True)
class Meta:
model = Return
fields = "__all__"
def create(self, validated_data):
items_objects = validated_data.pop('kits', None)
prdcts = []
for item in items_objects:
i = ReturnKits.objects.create(**item)
prdcts.append(i)
instance = Return.objects.create(**validated_data)
print("prdcts", prdcts)
instance.items.set(prdcts)
return instance
But I am not sure how to do serialization in the above mentioned scenario. Please Help!!
You can try something like this:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = ReturnKitsProducts
fields = "__all__"
class ReturnKitsSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True) # same as
class Meta:
model = ReturnKits
fields = "__all__"
class ReturnSerializer(serializers.ModelSerializer):
kits = ReturnKitsSerializer(many=True)
class Meta:
model = Return
fields = "__all__"
def create(self, validated_data):
items_objects = validated_data.pop('kits', None)
instance = Return.objects.create(**validated_data)
for item in item_objects:
return_products = item.pop('items')
i = ReturnKits.objects.create(**item)
for data in return_products:
return_product = ReturnKitsProducts.objects.create(**data)
i.items.add(return_product)
instance.items.add(i)
return instance
What I did was pulling out the data from the validated_data dictionary and create instances as necessary.
While the other answer works (for create and get), it is always better to use serializers to save data when possible. In future you may need some kind of custom validation or you may need to override default create method, for nested serializer.
If you don't do following, you will miss out all functions serializer class provides and may need to write your all code on your own in create function of your main serializer, which will be dirty.
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = ReturnKitsProducts
fields = "__all__"
class ReturnKitsSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True) # same as
class Meta:
model = ReturnKits
fields = "__all__"
def validate_items(self, value):
serializer = ItemSerializer(data=value, many=True)
if serializer.is_valid():
return serializer # this will be finally received and used in create function of main serializer
else:
raise serializers.ValidationError(serializer.errors)
def create(self, validated_data):
items = validated_data.pop('items')
r = ReturnKits.objects.create(**validated_data)
for item in items:
r.items.add(item.save()) # save each serializer (already validated) and append saved object
return r
class ReturnSerializer(serializers.ModelSerializer):
kits = ReturnKitsSerializer(many=True)
def validate_kits(self, value):
serializer = ReturnKitsSerializer(data=value, many=True)
if serializer.is_valid():
return serializer
else:
raise serializers.ValidationError(serializer.errors)
class Meta:
model = Return
fields = "__all__"
def create(self, validated_data):
# IMP: At this point all the data (including nested) will have been validated, so no error will throw when saving the data
kits = validated_data.pop('kits', None)
instance = Return.objects.create(**validated_data)
for kit in kits:
i = kits.save() # save each serializer (already validated) and get a object
instance.kits.add(i)
return instance
This code will work for create and get. If you want to update all your data at once, you should develop a approach.
You can try approach I use:
Updating data with its nested data
If the dictionary contains id field, the corresponding nested data will be updated.
If the dictionary does not contain id, field , new nested data will be created.
If the dictionary contains only id as key, the nested data will be deleted.
There is no need to provide nested data which do not need to be updated or deleted.
https://github.com/SagarKAdhikari/drf-nested-relations : A library where you can validate and save/update nested data to any depth, though it works for generic relation and foreign keys only for now( since only that was required in my project). You can try understanding the code and implment, if your nested relations are much deeper than at current and you have too many of them.
I have a Django model named BankDetail that has a one to one relationship with User.
#BankeDetails
class BankDetail(models.Model):
account_holder_name = models.CharField(max_length=100)
account_number = models.CharField(max_length=50, blank=True, null=True)
iban = models.CharField("IBAN", max_length=34, blank=True, null=True)
bank_name = models.CharField(max_length=100)
bank_address = models.CharField(max_length=500)
swift_bic_code = models.CharField(max_length=11)
user = models.OneToOneField(MyUser,on_delete=models.CASCADE)
accepting_fiat_currency = models.OneToOneField(AcceptedFiatCurrency)
created_at = models.DateTimeField(auto_now_add=True,null=True)
updated_at = models.DateTimeField(auto_now=True)
The serializer is as listed below :
class BankDetailSerializer(serializers.ModelSerializer):
"""Serializer for BankDetails"""
class Meta:
model = BankDetail
fields = "__all__"
def validate(self, data):
if data['account_number'] or data['iban']:
raise serializers.ValidationError("Please fill Account Number or IBAN")
return data
Request Payload :
{
"account_holder_name":"Aladin",
"account_number":"1239893",
"bank_name":"Aladin Bank",
"bank_address":"Republic of Wadia",
"swift_bic_code":"1",
"user_id":"1",
"accepting_fiat_currency_id":"1"
}
Now when I'm trying to save the model from my view, I get the following error :
{
"user": [
"This field is required."
],
"accepting_fiat_currency": [
"This field is required."
]
}
How can I pass I refrence ob these objects, do I need to manually retrieve them from the db using the id's?
AFAIR you should define a related model field in the serializer. And don't use __all__ instead of explicit write all fields. It's my recommendation :)
You should find answer in following questions:
Post from StackOverflow
That helped me last week
DRF documentation about Serializer relations
One solution is to use different serializer for creating/retrieving data in/from database.
Your serializer for creating should be something like -
class BankDetailSerializer(serializers.ModelSerializer):
"""Serializer for BankDetails"""
class Meta:
model = BankDetail
exclude = ('user', 'accepting_fiat_currency', ) # Note we have excluded the related fields
def validate(self, data):
if data['account_number'] or data['iban']:
raise serializers.ValidationError("Please fill Account Number or IBAN")
return data
Then while saving the serializer pass the above two excluded fields. Like -
serializer = BankDetailSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user, accepting_fiat_currency=<AcceptedFiatCurrency-object>)
If you are using Generic Views in DRF then I would suggest you to look
into perform_create method.
You need to send "user":"1". instead of "user_id":"1". The same with other "accepting_fiat_currency"
If I have the following Dango model and Django REST serializer:
# model
class Attribute(models.Model):
name = models.CharField(max_length=50)
code = models.CharField(max_length=50)
value = models.IntegerField(default=0)
# serializer
class AttributeSerializer(serializers.ModelSerializer):
name = serializers.CharField()
code = serializers.CharField()
value = serializers.IntegerField()
class Meta:
model = Attribute
fields = ('name', 'code', 'value', 'group')
Is it possible to accept a different field during the PUT or POST to update the model? for example, could it accept attribute_value and use that to update the value field?
There is to_internal_value function read more on Docs:
Override this to support deserialization, for write operations.
You can override it like this:
def to_internal_value(self, data):
if data.get('attribute_value'):
data['value'] = data.pop('attribute_value')
data = super().to_internal_value(data)
return data
My question is somewhat related to this one with some differences. I have a model similar to this one:
class Project(models.Model):
project_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
created_by_id = models.ForeignKey('auth.User', related_name='project', on_delete=models.SET_NULL, blank=True, null=True)
created_by = models.CharField(max_length=255, default="unknown")
created = models.DateTimeField(auto_now_add=True)
With the following serializer:
class ProjectSerializer(serializers.ModelSerializer):
created_by = serializers.ReadOnlyField(source='created_by_id.username')
class Meta:
model = Project
fields = ('project_id', 'created_by', 'created')
And corresponding view:
class projectsView(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(created_by_id=self.request.user)
This code behaves like I want but forces information redundancy and does not leverage the underlying relationnal database. I tried to use the info from the linked question to achieve a "write user id on database but return username on "get"" in a flat json without success:
Removing the "created_by" field in the model. Replacing the serializer with:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
class ProjectSerializer(serializers.ModelSerializer):
created_by = UserSerializer(read_only=True)
created_by_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), source='created_by', write_only=True)
class Meta:
model = Project
fields = ('project_id', 'created_by', 'created_by_id', 'created')
Which would NOT 100% give me what I want, i.e. replace the user id with the username in a flat json but return something like: {'project_id': <uuid>, 'created_by': <user json object>, 'created': <data>}. But still I get a {'created_by_id': ['This field is required.']} 400 error.
Question: How can I write a user id to a database object from the request.user information to refer to an actual user id but return a simple username in the GET request on the projectsView endpoint without explicitly storing the username in the Model? Or more generally speaking, how can I serialize database objects (Django models) into customer json response by using default serialization DRF features and default DRF views mixins?
Alternate formulation of the question: How can I store an ID reference to another DB record in my model (that can be accessed without it being supplied by the payload) but deserialize a derived information from that object reference at the serializer level such as one specific field of the referenced object?
I would recommend you to use Two different serializers for Get and POST operations. Change your serializers.py as
class ProjectGetSerializer(serializers.ModelSerializer):
created_by_id = serializers.StringRelatedField()
class Meta:
model = Project
fields = '__all__'
class ProjectCreateSerializer(serializers.ModelSerializer):
created_by_id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), default=serializers.CurrentUserDefault())
def create(self, validated_data):
return Project.objects.create(**validated_data, created_by=validated_data['created_by_id'].username)
class Meta:
model = Project
fields = '__all__'
Also, I reccomend ModelViewSet for API class if you are looking for CRUD operations. Hence the view will be like this,
class projectsView(viewsets.ModelViewSet):
queryset = Project.objects.all()
def get_serializer_class(self):
if self.action == 'create':
return ProjectCreateSerializer
return ProjectGetSerializer
So, the payload to create Project is,
{
}
One thing you should remember, while you trying to create Project user must logged-in
UPDATE - 1
serializer.py
class ProjectCreateSerializer(serializers.ModelSerializer):
created_by_id = serializers.StringRelatedField()
class Meta:
model = Project
fields = '__all__'
def create(self, validated_data):
return Project.objects.create(**validated_data, created_by_id=self.context['request'].user)
views.py
class projectsView(viewsets.ModelViewSet):
queryset = Project.objects.all()
serializer_class = ProjectCreateSerializer
The error is in the write_only field options. The required parameter default value is set to True while the intent is to not make it required if we take a look at the model. Here in the view, I use the perform_create as post processing to save on the Model DB representation. Since required default value is True at the creation level, the first .save() to the DB fails. Since this is purely internal logic, the required is not necessary. So simply adding the required=False option on the PrimaryKeyRelatedField does the job:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
class ProjectSerializer(serializers.ModelSerializer):
created_by = UserSerializer(read_only=True)
created_by_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), source='created_by', write_only=True, required=False)
class Meta:
model = Project
fields = ('project_id', 'created_by', 'created_by_id', 'created')
Enforcing the required=True at the Model level as well would require to override the .save function of the serializer if I insist on playing with the logic purely at the serializer level for deserialization. There might be a way to get the user ref within the serializer as well to keep the views implementation even more 'default'... This can be done by using the default value from Jerin:
class ProjectSerializer(serializers.ModelSerializer):
created_by = UserSerializer(read_only=True)
created_by_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), source='created_by',
write_only=True,
required=False,
default=serializers.CurrentUserDefault())
class Meta:
model = Project
fields = ('project_id', 'created_by', 'created_by_id', 'created')
Now to flaten the json with username only, you need to use a slug field instead of the UserSerializer:
class ProjectSerializer(serializers.ModelSerializer):
created_by = serializers.SlugRelatedField(
queryset=User.objects.all(), slug_field="username")
created_by_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), source='created_by', write_only=True, required=False)
class Meta:
model = Project
fields = ('project_id', 'created_by', 'created_by_id', 'created')
And then only the username field value of the User Model will show at the create_by json tag on the get payload.
UPDATE - 1
After some more tweaking here is the final version I came up with:
class ProjectSerializer(serializers.ModelSerializer):
created_by_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), write_only=True, required=False, default=serializers.CurrentUserDefault())
created_by = serializers.SerializerMethodField('creator')
def creator(self, obj):
return obj.created_by_id.username
class Meta:
model = Project
fields = ('project_id', 'created_by_id', 'created_by', 'created')
I have a model:
class EventTracker(models.Model):
"""
Track events of user's behaviors
"""
class Meta:
verbose_name = "EventTracker"
verbose_name_plural = "EventTrackers"
unique_together = ("application", "label")
application = models.ForeignKey(Application, related_name='events')
label = models.CharField(max_length=50)
count = models.PositiveIntegerField(default=0)
value = models.IntegerField(null=True)
def __str__(self):
return "[{}] {}".format(self.application, self.label)
This is my serializer for this model:
class EventTrackerSerializer(serializers.ModelSerializer):
subscriber_id = serializers.IntegerField(min_value=1)
class Meta:
model = EventTracker
fields = ('id', 'application', 'label', 'count', 'value', 'subscriber_id')
write_only_fields = ('subscriber_id', )
read_only_fields = ('count',)
subscriber_id is a field that doesn't belong to this model. But request data must have subscriber_id to do a thing. So I want to validate it in serializer. I don't know how to validate it. I tried like above, it threw error:
This may be because you have a writable field on the serializer class that is not a valid argument to.....
So what can I do ?
First, you should probably be more explicit about what you want to do. We don't know what that field is for nor if it's readable nether what/how you want to validate it, so I'd do some guesswork.
Assuming it's write only:
subscriber_id = serializers.IntegerField(min_value=1, write_only=True)
Note that the write_only_fields has been removed for some time.
Next, you'll have to write manually the serializer's create/update. Example for the create:
class EventTrackerSerializer(serializers.ModelSerializer):
...
def create(self, validated_data):
subscriber_id = validated_data.pop('subscriber_id')
instance = EventTracker.objects.create(**validated_data)
return instance