Django Rest Framework Nested Serializer Existing Instance - python

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}, }

Related

Django rest framework nested serializer create method

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)

How do you create a deferred field with a SerializerMethodField?

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'}

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

access model field in serializer

key and data are dictionary how I can access data of specific key in serializer
class Setting(models.Model):
key = models.CharField(max_length=255, primary_key=True)
data = JSONField(null=True, blank=True)
In serializer, something like this
from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
class SettingsSerializer(serializers.ModelSerializer):
sample_filed = serializers.SerializerMethodField()
class Meta:
model = Settings
fields = ('key', 'data', 'sample_filed')
def get_sample_field(self):
""" a sample field function to demonstrate srialzer method"""
""" you can access all keys here as self.key "
return self.key + self.data
Note:- By using model serializer you will just have to specify the model. then restframework will handle all generic stuff.
you can use serializer methodfield() if you want extra field in the response. there you can use the key as stated above
(or) you can use django -orm
you can use django orm queries outside the serializer.
to get a specific row
Settings.objects.get(key = 'key-value') (only if key is unique. this will throw an exception if there is no value)
(or)
Settings.objects.filter(key = 'key-value') (recommded. will give you all the results.)
Settings.objects.all() will give you all records
Settings.objects.filter(key = 'sdaf', data = 'sds') Specific records with key and data .
work with this code
ssh_instance = Setting.objects.get(key='ssh-port')
print(ssh_instance.data["value"])

Django 2.1: How to get a list of objects that in each object, contains the objects that have a foreign key pointing to them

I have these two classes:
Class A:
data
Class B:
a = models.ForeignKey(A)
How can I get an array of A in which each A contains the B's related to them?
I need it because I must return the two tables joined in a JSON.
First set the back relation of a and then migrate the database:
Class A(models.Model):
data
Class B(models.Model):
a = models.ForeignKey(A, related_name='b_relations', null=False, blank=False, on_delete=models.CASCADE)
Now you can access the back relation. You need to call .all() on b_relations because it is lazy loaded.
A.objects.first().b_relations.all()
To get json data I suggest to use django rest framework's ModelSerializer:
class BSerializer(serializers.ModelSerializer):
class Meta:
model = B
fields = (
'pk', # primary key
'a',
)
depth = 0
class ASerializer(serializers.ModelSerializer):
b_relations = BSerializer(many=True, read_only=True))
class Meta:
model = A
fields = (
'pk', # primary key
'b_relations',
)
depth = 0
To get json data:
a_relations: QuerySet = A.objects.all()
serializer = ASerializer(a_relations, many=True)
json_data: Dict = serializer.data
In case you've never used Django Rest Framework: Django Rest Framework - Getting Started
Have fun!

Categories

Resources