I want to create a deferred method field in a model serializer using drf-flexfields.
I am using Django Rest Framework and drf-flexfields. I want to create a method field in my model serializer to make a complex query. Because retrieving this field will incur extra database lookups, I want this to be a deferred field, i.e. it will only be retrieved if the client specifically asks for it.
The DRF-Flexfields documentation seems to infer that a field can be deferred by only listing it in "expanded_fields" and not in the normal "fields" list, but gives no further explanation or example. https://github.com/rsinger86/drf-flex-fields#deferred-fields
I have tried creating a simple SerializerMethodField to test this:
class Phase(models.Model):
name = models.CharField(max_length=100)
assigned = models.ManyToManyField(settings.AUTH_USER_MODEL,
blank=True,
related_name="phases_assigned")
class PhaseSerializer(FlexFieldsModelSerializer):
assignable_users = serializers.SerializerMethodField("get_assignable_users")
expandable_fields = {
'assignable_users': (UserSerializer, {'source': 'assignable_users', 'many': True}),
}
class Meta:
model = Phase
fields = ['name', 'assigned']
def get_assignable_users(self, phase):
return {'test': 'this is a deferred field. It should only shows up when /?expand=assigned_users is given in '
'the api get request url'}
I get the following error :
"The field 'assignable_users' was declared on serializer PhaseSerializer, but has not been included in the 'fields' option."
the desired result would be that a call to the api at /phase/ will return just the default fields specified in the meta "fields" list. "assignable_users" will only get returned if the client specifically asks for it with /phase/?expand=assignable_users.
What is the correct way to go about accomplishing this?
No matter it is a SerializerMethodField, you should add your assignable_users field in Meta fields:
class PhaseSerializer(FlexFieldsModelSerializer):
...
class Meta:
model = Phase
fields = ['name', 'assigned', 'assignable_users']
# _____________________________^
Check the docs for more information.
If you declare a field in the serializer, you must include it in the fields option, as the error says. But if you do that, the field will be shown by default.
So, if you want a deferred field (on demand field), you can declare it in the model as a property:
class MyModel(models.Model):
my_field1 = ...
my_field2 = ...
my_field3 = ...
...
#property
def deferred_field(self):
return 'extra database lookups'
Then, in your serializer, you include it in expandable_fields as serializers.StringRelatedField:
class MySerializer(FlexFieldsModelSerializer):
class Meta:
model = MyModel
fields = ['my_field1', 'my_field2']
expandable_fields = {
'my_field3': (serializers.StringRelatedField),
'deferred_field': (serializers.StringRelatedField),
}
class PhaseSerializer(FlexFieldsModelSerializer):
...
expandable_fields = {
'assignable_users': (serializers.SerializerMethodField, {'read_only': True}),
'assignable_users_m2m': (UserSerializer, {'source': 'assigned', 'many': True}),
}
class Meta:
model = Phase
fields = ['name', 'assigned']
def get_assignable_users(self, phase):
return {'test': '1111'}
Related
info: One is User and the Second one is User Data like phone number,city etc
Now i can add these two serializer into third serializer and show all data in third one
Problem: now i want to create new Serializer Where i can combine both serializer attributes. but i don't understand how can i achieve to bind two serializer into different one and show all data?
serializers.py
class ContactSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
fields = ['email', 'phone']
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = ['city']
Want to achieve
class CombineSerializer(serializers.ModelSerializer):
class Meta:
fields = ['email', 'phone', 'city']
You can simply create an object of a LocationSerializer in your contact serializer. This approach will create a nested object of location serializer.
class ContactSerializer(serializers.ModelSerializer):
location = LocationSerializer() #if the contact serializer have multiple city then just add `many=True` as an argument.
class Meta:
model = Contact
fields = ['email', 'phone', 'location']
While listing the object will look like this -
{
'email':'some#email.com',
'phone':'00009999888',
'location':{
'city':'some_city'
}
}
However, if you want to have both objects as a nested object than in that case you can simply call the serializer in the CombineSerializer class. But here instead of using serializers.ModelSerializer you will have to use serializers.Serializer as no model is associated with it.
This is how you can achieve it -
class CombineSerializer(serializers.Serializer):
contact = ContactSerializer()
location = LocationSerializer()
In this case while listing the object will look like this -
{
'contact' : {
'email' : 'some#email.com',
'phone' : '00999887666'
},
'location' : {
'city' : 'Some City'
}
}
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)
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
I have a nested ModelSerializer that I'm having trouble validating.
The problem I'm running into is that upon parent serializer creation I may or may not need to create the nested serializer/model as it may already exist in the database and I just want to link to it.
Code Setup:
models.py
class ModelA(models.Model):
modelb = ForeignKey(ModelB, null=true, blank=true)
...
class ModelB(models.Model):
...
serializers.py
class ModelASerializer(serializers.ModelSerializer):
modelb = ModelBSerializer(required=False)
class Meta:
model = ModelA
depth = 1
class ModelBSerializer(serializers.ModelSerializer):
class Meta:
model = ModelB
So, given 3 data scenarios I run into validation errors on all 3.
First, if I pass the NestedModel as data like so
data = {
'nestedmodel': NestedModel(**args),
...
}
I get the validation error saying there was a non_field_error and that it was expecting a dictionary but got a NestedModel instance.
Second, if I pass the data of the NestedModel (instead of the object):
data = {
'nestedmodel': {'id': 'this', ... },
}
I get the validation error equivalent of a duplicate key since the Nested Model has a unique key ('id') and that already exists in the database.
And third, if I just pass it the id of the nestedmodel, I get a similar error to the first situation except it says it got Unicode instead of the NestedModel instance.
data = {
'nestedmodel': 'this',
}
I understand why all three of these situations are happening and validation is failing, but that doesn't help me in my goal of trying to link an already existing NestedModel.
How do I go about doing that? What am I doing wrong?
Can you try this:
serializers.py
class ModelASerializer(serializers.ModelSerializer):
modelb = ModelBSerializer(required=False)
class Meta:
model = ModelA
depth = 1
fields = ('id', 'modelb', )
def create(self, validated_data):
modelb_id = self.validated_data.pop("nestedmodel")
modelb = ModelB.objects.get(id=modelb_id["id"])
modela = ModelA.objects.create(modelb=modelb, **validated_data)
return modela
Pass the data as follows:
Input
data = {"nestedmodel": {"id": 1 # add nestedmodel fields here}, }
So I have the following Structure:
A ClientFile belongs to an Owner (class name = Contact).
I'm trying to create a Clientfile using the API. The request contains the following data:
{
name: "Hello!"
owner: {
id: 1,
first_name: "Charlie",
last_name: "Watson"
}
}
I created the serializer according to my structure. Hoping that this API call would create a clientfile with the name "Hello!" and Contact id 1 as the owner:
class ContactSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
fields = (
'id',
'first_name',
'last_name',
)
class ClientfileSerializer(serializers.ModelSerializer):
owner = ContactSerializer(read_only=False)
class Meta():
model = Clientfile
fields = (
'id',
'name',
'owner',
)
def create(self, validated_data):
owner = Contact.objects.get(pk=validated_data['owner']['id'])
I do get into the create method. However, the only field I need (['owner']['id']) is not accessible. If I do print ['owner']['first_name'] it does return 'Charlie'. But the ID for some reasons doesn't seem to be accessible...
Any reasons why this can be happening? Am i missing something? (I'm new to Django)
SOLUTION: Just found out that the reason why ID didn't show in the first place was because I had to declare it in the fields like so: Hope this helps.
class ContactSerializer(serializers.ModelSerializer):
id = serializers.IntegerField() # ← Here
class Meta:
model = Contact
fields = (
'id',
'first_name',
'last_name',
)
In Django REST Framework AutoField fields (those that are automatically generated) are defaulted to read-only. From the docs:
read_only
Set this to True to ensure that the field is used when
serializing a representation, but is not used when creating or
updating an instance during deserialization.
Defaults to False
You can see this by inspecting your serializer by printing the representation in your shell:
serializer = ClientfileSerializer()
print repr(serializer)
You can override this by setting read_only=False against the id field in the extra_kwargs:
class ContactSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
fields = (
'id',
'first_name',
'last_name',
)
extra_kwargs = {'id': {'read_only': False}}
class ClientfileSerializer(serializers.ModelSerializer):
owner = ContactSerializer(read_only=False)
class Meta():
model = Clientfile
fields = (
'id',
'name',
'owner',
)
extra_kwargs = {'id': {'read_only': False}}
Alright so I found a different approach that works.
I added an IntegerField serializer for the owner relation. I also had to set the owner relation to read_only=True.
This is the json I am sending via POST:
{
name: "Hello!"
owner_id: 1
}
This is my serializer:
class ClientfileSerializer(serializers.ModelSerializer):
owner_id = serializers.IntegerField()
owner = ContactSerializer(read_only=True)
class Meta():
model = Clientfile
fields = (
'id',
'owner_id',
'owner',
)
It seems less cool than the first way, but it does the job.
Plus I don't want to create a new owner, but just select one that is already in the database. So maybe it's more semantic to only have the ID and not the full set of information posted via Json.
You can try something like this:
class YourModelSerializer(serializers.ModelSerializer):
class Meta:
model = YourModel
fields = ('id', 'field1', 'field2')
def to_internal_value(self, data):
"""
Dict of native values <- Dict of primitive datatypes.
Add instance key to values if `id` present in primitive dict
:param data:
"""
obj = super(YourModelSerializer, self).to_internal_value(data)
instance_id = data.get('id', None)
if instance_id:
obj['instance'] = YourModel.objects.get(id=instance_id)
return obj
Then in serializer validated data you should have "instance" key if request.data has "id" key.
Also You can add just "id" instead of full instance/object.
The top voted answer does solve the issue but it raises a new one as mentioned in the comments we can no longer create a new record as it will thrown ac exception saying is required. We can set id to required=False then id will be available in validated_data and it wont be required to set it manually
id = serializers.IntegerField(required=False) <- Like this
class Meta:
model = Details
fields = ('id', 'product_name', 'description', 'specification', 'make_model',
'brand', 'quantity',)