Django rest framework nested serializer create method - python

I have created a nested serializer, when I try to post data in it it keeps on displaying either the foreign key value cannot be null or dictionary expected. I have gone through various similar questions and tried the responses but it is not working for me. Here are the models
##CLasses
class Classes(models.Model):
class_name = models.CharField(max_length=255)
class_code = models.CharField(max_length=255)
created_date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.class_name
class Meta:
ordering = ['class_code']
##Streams
class Stream(models.Model):
stream_name = models.CharField(max_length=255)
classes = models.ForeignKey(Classes,related_name="classes",on_delete=models.CASCADE)
created_date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.stream_name
class Meta:
ordering = ['stream_name']
Here is the view
class StreamViewset(viewsets.ModelViewSet):
queryset = Stream.objects.all()
serializer_class = StreamSerializer
Here is the serializer class
class StreamSerializer(serializers.ModelSerializer):
# classesDetails = serializers.SerializerMethodField()
classes = ClassSerializer()
class Meta:
model = Stream
fields = '__all__'
def create(self,validated_data):
classes = Classes.objects.get(id=validated_data["classes"])
return Stream.objects.create(**validated_data, classes=classes)
# def perfom_create(self,serializer):
# serializer.save(classes=self.request.classes)
#depth = 1
# def get_classesDetails(self, obj):
# clas = Classes.objects.get(id=obj.classes)
# classesDetails = ClassSerializer(clas).data
# return classesDetails
I have tried several ways of enabling the create method but like this displays an error {"classes":{"non_field_errors":["Invalid data. Expected a dictionary, but got int."]}}. Any contribution would be deeply appreciated

This is a very common situation when developing APIs with DRF.
The problem
Before DRF reaches the create() method, it validates the input, which I assume has a form similar to
{
"classes": 3,
"stream_name": "example"
}
This means that, since it was specified that
classes = ClassSerializer()
DRF is trying to build the classes dictionary from the integer. Of course, this will fail, and you can see that from the error dictionary
{"classes":{"non_field_errors":["Invalid data. Expected a dictionary, but got int."]}}
Solution 1 (requires a new writable field {field_name}_id)
A possible solution is to set read_only=True in your ClassSerializer, and use an alternative name for the field when writing, it's common to use {field_name}_id. That way, the validation won't be done. See this answer for more details.
class StreamSerializer(serializers.ModelSerializer):
classes = ClassSerializer(read_only=True)
class Meta:
model = Stream
fields = (
'pk',
'stream_name',
'classes',
'created_date',
'classes_id',
)
extra_kwargs = {
'classes_id': {'source': 'classes', 'write_only': True},
}
This is a clean solution but requires changing the user API. In case that's not an option, proceed to the next solution.
Solution 2 (requires overriding to_internal_value)
Here we override the to_internal_value method. This is where the nested ClassSerializer is throwing the error. To avoid this, we set that field to read_only and manage the validation and parsing in the method.
Note that since we're not declaring a classes field in the writable representation, the default action of super().to_internal_value is to ignore the value from the dictionary.
from rest_framework.exceptions import ValidationError
class StreamSerializer(serializers.ModelSerializer):
classes = ClassSerializer(read_only=True)
def to_internal_value(self, data):
classes_pk = data.get('classes')
internal_data = super().to_internal_value(data)
try:
classes = Classes.objects.get(pk=classes_pk)
except Classes.DoesNotExist:
raise ValidationError(
{'classes': ['Invalid classes primary key']},
code='invalid',
)
internal_data['classes'] = classes
return internal_data
class Meta:
model = Stream
fields = (
'pk',
'stream_name',
'classes',
'created_date',
)
With this solution you can use the same field name for both reading and writing, but the code is a bit messy.
Additional notes
You're using the related_name argument incorrectly, see this question. It's the other way around,
classes = models.ForeignKey(
Classes,
related_name='streams',
on_delete=models.CASCADE,
)
In this case it should be streams.

Kevin Languasco describes the behaviour of the create method quite well and his solutions are valid ones. I would add a variation to solution 1:
class StreamSerializer(serializers.ModelSerializer):
classes = ClassSerializer(read_only=True)
classes_id = serializers.IntegerField(write_only=True)
def create(self,validated_data):
return Stream.objects.create(**validated_data, classes=classes)
class Meta:
model = Stream
fields = (
'pk',
'stream_name',
'classes',
'classes_id',
'created_date',
)
The serializer will work without overriding the create method, but you can still do so if you want to as in your example.
Pass the value classes_id in the body of your POST method, not classes. When deserializing the data, the validation will skip classes and will check classes_id instead.
When serializing the data (when you perform a GET request, for example), classes will be used with your nested dictionary and classes_id will be omitted.

You can also solve this issue in such a way,
Serializer class
# Classes serializer
class ClassesSerializer(ModelSerializer):
class Meta:
model = Classes
fields = '__all__'
# Stream serializer
class StreamSerializer(ModelSerializer):
classes = ClassesSerializer(read_only=True)
class Meta:
model = Stream
fields = '__all__'
View
# Create Stream view
#api_view(['POST'])
def create_stream(request):
classes_id = request.data['classes'] # or however you are sending the id
serializer = StreamSerializer(data=request.data)
if serializer.is_valid():
classes_instance = get_object_or_404(Classes, id=classes_id)
serializer.save(classes=classes_instance)
else:
return Response(serializer.errors)
return Response(serializer.data)

Related

DRF: Conditially change serializer

I'm using Django 3.0 and I have a serializer using django-rest-framework. Let's say that for example I have a Forum object. Each forum has an owner that is a user.
In my GET /forums/ endpoint, I'd like to just have the owner_id. However, in my GET /forums/<forum_id>/ endpoint I'd like to return the entire embedded object.
Is there any way to have one serializer support both of these scenarios? If not, I would hate to have to make two serializers just to support this.
class ForumSerializer(serializers.ModelSerializer, compact=True):
if self.compact is False:
owner = UserSerializer(source='owner', read_only=True)
else:
owner_id = serializers.UUIDField(source='owner_id')
...
How can I achieve this compact thing?
class Meta:
fields = [...]
read_only_fields = ['owner', 'owner_id']
You can add a SerializerMethodField like this:
class ForumSerializer(serializers.ModelSerializer):
owner = serializer.SerializerMethodField()
def get_owner(self, obj):
if self.context['is_compact'] == True:
return obj.owner.pk
else:
return UserSerializer(obj.owner).data
class Meta:
model = YourModel
fields = '__all__'
# Usage in view
serializer = ForumSerializer(context={'is_compact':True})
I am passing is_compact value through serializer's extra context.
create two serializer classes
class ForumSerializerId(ModelSerializer):
class Meta:
model = Forum
fields = ['forum_id']
class ForumSerializerDetail(ModelSerializer):
class Meta:
model = Forum
on your view.py
forums(request):
forum_list = Forum.objects.all()
forum_serializer = ForumSerializerId(forum_list,many=True)
return Response({"form":forum_serializer.data})
forum_detail(request,pk):
forum = get_object_or_404(Forum,pk)
forum_serializer = ForumSerializerDetail(forum)
return Response({"form":forum_serializer.data})

How to get model data to appear as a field in another model's response

These are simplified versions of my models (the user model is just an id and name)
class Convo(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='convo_owner')
users = models.ManyToManyField(User, through='Convo_user')
class Convo_user (models.Model):
user = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
convo = models.ForeignKey(Convo, on_delete=models.CASCADE)
class Comments(models.Model):
name = models.CharField(max_length=255)
content = models.TextField(max_length=1024)
convo = models.ForeignKey(Convo, on_delete=models.CASCADE)
This is my view
class ConvoViewSet(viewsets.ModelViewSet):
serializer_class = serializers.ConvoSerializer
def get_queryset(self):
return None
def list(self, request):
curr_user = request.user.id
# Collecting the list of conversations
conversations = models.Conversation.object.filter(ConvoUser__user_id=request.user.id)
#Getting list of conversation id's
conv_ids = list(conversations.values_list('id', flat=True).order_by('id'))
#Getting list of relevant comments
comments = models.Comments.objects.filter(conversation_id__in=conv_ids)
return Response(self.get_serializer(conversations, many=True).data)
And my current serializer
class ConvoSerializer(serializers.ModelSerializer):
"""A serializer for messaging objects"""
# access = AccessSerializer(many=True)
# model = models.Comments
# fields = ('id', 'name', 'content', 'convo_id')
class Meta:
model = models.Convo
fields = ('id', 'owner_id')
The current response I get is of the form
[
{
"id": 1,
"owner_id": 32
}, ...
]
But I would like to add a comments field that shows all the properties of comments into the response, so basically everything in the second queryset (called comments) and I'm not sure how to go about this at all. (I retrieve the comments in the way I do because I'm trying to minimize the calls to the database). Would I need to create a new view for comments, make its own serializer and then somehow combine them into the serializer for the convo?
The way you've set up your models, you can access the comments of each Convo through Django's ORM by using convo_object.comments_set.all(), so you could set up your ConvoSerializer to access that instance's comments, like this:
class ConvoSerializer(serializers.ModelSerializer):
"""A serializer for messaging objects"""
comments_set = CommentSerializer(many=True)
class Meta:
model = models.Convo
fields = ('id', 'owner_id', 'comments_set')
and then you define your CommentSerializer like:
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = models.Comments
fields = ('id', 'name', 'content')
No data appears because my serializers are using the default database, not sure why but a step forward
EDIT:
Django: Database used for prefetch_related is not the same that the parent query Provided me the correct answer, I was able to choose the database with this method because for some reason inner queries use the default DB

Django rest framework represent flatten nested object

I have a parent and a one-to-one related child model and I would like to render the fields from the child flat in the parent representation (read only). Currently, I have achieved that with a custom to_representation implementation but that seems very involved and I wonder if there is no easier way to achieve this.
It is made more complicated by the fact that my related model is connected via a property.
So here is the concrete example:
By default a related object would be rendered like:
{
parent_name:'Bob',
child:{
name:'Alice'
}
}
This is what I want and currently get with my to_representation:
{
parent_name:'Bob',
child_name:'Alice'
}
My models look like this:
class ChildModel(models.Model):
name = models.CharField(max_length=100, null=True)
class ParentModel(models.Model):
name = models.CharField(max_length=100, null=True)
_child = models.ForeignKey('ChildModel', null=True)
#property
def child(self):
return self._most_recent_status
#name.setter
def child(self, value):
self._child = value
Here are my serializers:
class FlatChildField(serializers.RelatedField):
def to_representation(self, value):
return value.name
class FlatParentSerializer(serializers.ModelSerializer):
parent_name = serializers.CharField(source='name', read_only=True)
child_name = FlatChildField(source='_child', read_only=True)
class Meta:
model = Parent
fields = ('name', 'child_name')
For a simpler solution to get a flat representation of related models I would be grateful.
For completeness, I would be interested to hear if there is a simpler solution for "normal" related models (i.e. not property model fields as well). I was looking for the equivalent of the django model query syntax of related_model__field, but I cannot find that. Does that exist for django rest framework?
Many thanks
The simplest means would be to use source:
class FlatParentSerializer(serializers.ModelSerializer):
parent_name = serializers.CharField(source='name', read_only=True)
child_name = serializers.CharField(source='_child.name', read_only=True)
class Meta:
model = Parent
fields = ('name', 'child_name')
You can use SerializerMethodField, it saves you really a lot of work and it's so clean and trivial:
class FlatParentSerializer(serializers.ModelSerializer):
parent_name = serializers.CharField(source='name', read_only=True)
child_name = serializers.SerializerMethodField('get_child_name')
class Meta:
model = Parent
fields = ('name', 'child_name')
def get_child_name(self, obj):
return obj._child.name

Mixin common fields between serializers in Django Rest Framework

I have this:
class GenericCharacterFieldMixin():
attributes = serializers.SerializerMethodField('character_attribute')
skills = serializers.SerializerMethodField('character_skill')
def character_attribute(self, obj):
character_attribute_fields = {}
character_attribute_fields['mental'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.mental_attributes}
character_attribute_fields['physical'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.physical_attributes}
character_attribute_fields['social'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.social_attributes}
return character_attribute_fields
def character_skill(self, obj):
character_skill_fields = {}
character_skill_fields['mental'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.mental_skills}
character_skill_fields['physical'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.physical_skills}
character_skill_fields['social'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.social_skills}
return character_skill_fields
class MageSerializer(GenericCharacterFieldMixin, serializers.ModelSerializer):
player = serializers.ReadOnlyField(source='player.username')
arcana = serializers.SerializerMethodField()
def get_arcana(self, obj):
if obj:
return {str(arcana): arcana.current_value for arcana in obj.linked_arcana.all()}
class Meta:
model = Mage
fields = ('id', 'player', 'name', 'sub_race', 'faction', 'is_published',
'power_level', 'energy_trait', 'virtue', 'vice', 'morality', 'size',
'arcana', 'attributes', 'skills')
depth = 1
GenericCharacterFieldMixin is a Mixin of Fields for Characters, that are Generic, i.e. common to all types of characters.
I'd like my Mage Serializer to have these 'mixed in' rather than c/p then between all types of character (Mage is a type of character) hopefully this will increase DRYness in my webapp.
The issue is on the model I have this:
class NWODCharacter(models.Model):
class Meta:
abstract = True
ordering = ['updated_date', 'created_date']
name = models.CharField(max_length=200)
player = models.ForeignKey('auth.User', related_name="%(class)s_by_user")
....
def save(self, *args, **kwargs):
...
attributes = GenericRelation('CharacterAttributeLink')
skills = GenericRelation('CharacterSkillLink')
Which means I get this error:
TypeError at /characters/api/mages
<django.contrib.contenttypes.fields.create_generic_related_manager.<locals>.GenericRelatedObjectManager object at 0x00000000051CBD30> is not JSON serializable
Django Rest Framework thinks I want to serialize my generic relationship.
If I rename the fields in the model (s/attributes/foos/g, s/skills/bars/g) then I get a different (less clear?) error :
ImproperlyConfigured at /characters/api/mages
Field name `attributes` is not valid for model `ModelBase`.
How do I pull those methods and fields into a mixin, without confusing DRF?
Set SerializerMetaclass:
from rest_framework import serializers
class GenericCharacterFieldMixin(metaclass=serializers.SerializerMetaclass):
# ...
This is the solution recommended by DRF's authors.
Solutions suggested in the previous answers are problematic:
user1376455's solution hacks DRF into registering the mixin's fields in _declared_fields by declaring them on the child as different fields. This hack might not work in subsequent versions of the framework.
Nikolay Fominyh's solution changes the mixin to a fully fledged serializer (note that due to this, the name GenericCharacterFieldMixin is very unfortunate for a class which is not a mixin, but a serializer!). This is problematic because it takes the full Serializer class into the multiple inheritance, see the DRF issue for examples demonstrating why this is a bad idea.
Solution is simple as changing
class GenericCharacterFieldMixin():
to
class GenericCharacterFieldMixin(serializers.Serializer):
i had same issue and my google search brought me here. i managed to solve it.
since you are including attributes and skill fields in serialiser, you need to provide serialisation method for it.
this worked for me
class MageSerializer(GenericCharacterFieldMixin, serializers.ModelSerializer):
player = serializers.ReadOnlyField(source='player.username')
arcana = serializers.SerializerMethodField()
attributes = serializers.PrimaryKeyRelatedField(many=True,
read_only= True)
skills = serializers.PrimaryKeyRelatedField(many=True,
read_only= True)
def get_arcana(self, obj):
if obj:
return {str(arcana): arcana.current_value for arcana in obj.linked_arcana.all()}
class Meta:
model = Mage
fields = ('id', 'player', 'name', 'sub_race', 'faction', 'is_published',
'power_level', 'energy_trait', 'virtue', 'vice', 'morality', 'size',
'arcana', 'attributes', 'skills')
depth = 1

'Dynamic' fields in DRF serializers

My aim is to build endpoint which will surve to create objects of model with GenericForeignKey. Since model also includes ContentType, the actual type of model which we will reference is not known before object creation.
I will provide an example:
I have a 'Like' model which can reference a set of other models like 'Book', 'Author'.
class Like(models.Model):
created = models.DateTimeField()
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
Serializer may look like this:
class LikeSerializer(serializers.ModelSerializer):
class Meta:
model = models.Like
fields = ('id', 'created', )
What I want to achieve is to determine type of Like based on keys passed in request. The problem is that DRF do not pass those keys from request if they were not expilictly specified in Serializer fields. For example, POST request body contains:
{
"book":2
}
I want to do next
def restore_object(self, attrs, instance=None)
if attrs.get('book', None) is not None:
# create Like instance with Book contenttype
elif attrs.get('author', None) is not None:
# create Like instance with Author contenttype
In this case first if clause will be executed.
As you can see, The type determined based on key passed in request, without specifying special Field.
Is there any way to achieve this?
Thanks
You might try instantiating your serializer whenever your view is called by wrapping it in a function (you make a serializer factory):
def like_serializer_factory(type_of_like):
if type_of_like == 'book':
class LikeSerializer(serializers.ModelSerializer):
class Meta:
model = models.Like
fields = ('id', 'created', )
def restore_object(self, attrs, instance=None):
# create Like instance with Book contenttype
elif type_of_like == 'author':
class LikeSerializer(serializers.ModelSerializer):
class Meta:
model = models.Like
fields = ('id', 'created', )
def restore_object(self, attrs, instance=None):
# create Like instance with Author contenttype
return LikeSerializer
Then override this method in your view:
def get_serializer_class(self):
return like_serializer_factory(type_of_like)
Solution 1
Basically there is a method you can add on GenericAPIView class called get_context_serializer
By default your view, request and format class are passed to your serializer
DRF code for get_context_serializer
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
you can override that on your view like this
def get_serializer_context(self):
data = super().get_serializer_context()
# Get the book from post and add to context
data['book'] = self.request.POST.get('book')
return data
And use this on your serializer class
def restore_object(self, attrs, instance=None):
# Get book from context to use
book = self.context.get('book', None)
author = attrs.get('author', None)
if book is not None:
# create Like instance with Book contenttype
pass
elif author is not None:
# create Like instance with Author contenttype
pass
Solution 2
Add a field on your serializer
class LikeSerializer(serializers.ModelSerializer):
# New field and should be write only, else it will be
# return as a serializer data
book = serializers.IntegerField(write_only=True)
class Meta:
model = models.Like
fields = ('id', 'created', )
def save(self, **kwargs):
# Remove book from validated data, so the serializer does
# not try to save it
self.validated_data.pop('book', None)
# Call model serializer save method
return super().save(**kwargs)

Categories

Resources