Serializing a list of object as dictionnary in DjangoRestFramework - django

Using django & django-rest-framework, I have the following model (this is simplified but it's all there):
class Device(Model):
#stuff
class DeviceInformation(Model):
device = ForeignKey(Device, reverse='infos')
key = CharField(max_length=32)
value = CharField(max_length=1024)
When serializing a device through django-rest-framework's ModelSerializer, I get something like this:
{
//stuff
infos: [{
'key':'BatteryLevel',
'value':'80%'
},{
'key':'DeviceName',
'value':'my device'
}, //etc
]
}
Which is perfectly normal. However, it would make much more sense to serialize into something like this:
{
//stuff
infos: {
'BatteryLevel':'80%',
'DeviceName':'my device',
//etc
}
}
How do I do that? Is it even possible?
Note that I don't need to deserialize any of these information.
EDIT: my serializers are as follows:
class DeviceInfoSerializer(ModelSerializer):
class Meta:
model = DeviceInformation
fields = ('key', 'value')
read_only_fields = fields
class DeviceSerializer(HyperlinkedModelSerializer):
udid = serializers.CharField(read_only=True)
def __init__(self, *args, **kwargs):
super(DeviceSerializer, self).__init__(*args, **kwargs)
if hasattr(self, 'object') and self.object and not self.many:
self.data['infos'] = DeviceInfoSerializer(
self.object.infos.all(), many=True).data
class Meta:
model = Device
fields = ['udid', 'model', 'tracked']
read_only_fields = ('model', 'tracked')
slug_field = 'udid'

For your readonly-case, the best way is to use SerializerMethodField.
This would change your DeviceSerializer and remove the need for your DeviceInfoSerializer.
class DeviceSerializer(HyperlinkedModelSerializer):
udid = serializers.CharField(read_only=True)
infos = serializers.SerializerMethodField('get_infos')
def get_infos(self, obj):
return {
info.key: info.value
for info in obj.infos.all()
}
class Meta:
model = Device
fields = ['udid', 'model', 'tracked', 'infos']
read_only_fields = ('model', 'tracked', 'infos')
slug_field = 'udid'

Related

DjangoRestFramework, how to add optional field not coming from model to serializer

I have a model like this:
class Camper(models.Model):
location = models.PointField()
name = models.CharField(max_length=255)
and a viewset like this:
class CamperViewSet(viewsets.ModelViewSet):
...
def retrieve(self, request, *args, **kwargs):
"""Retrieve a Camper instance."""
show_weather = request.query_params.get('showWeather', False)
instance = self.get_object()
if show_weather:
lat = instance.location.y
lon = instance.location.x
instance.weather = getWeatherFromLatLon(lat, lon)
serializer = self.get_serializer(instance)
return Response(serializer.data)
So when I request /api/campers/8?showWeather=true I make another request in my view to get the weather from the current position.
How do I add it to my serializer ? It's an optional field so I need to manage this and it's only used in /campers/id so it will not be used in list/create/put/etc
My serializer looks like this:
class CamperSerializer(serializers.ModelSerializer):
camper_id = serializers.IntegerField(source='id')
class Meta:
model = Camper
fields = ('camper_id', 'name', 'location')
you can add custom serializer for retrive only todo it. I called CamperRetriveSerializer.
Inside CamperRetriveSerializer, you can use SerializerMethodField for define field not have in database.
And you want check param show_weather from request, best is pass value of it to context and get it in serializer.
Like this:
class CamperRetriveSerializer(serializers.ModelSerializer):
weather = serializers.SerializerMethodField()
camper_id = serializers.IntegerField(source='id')
def get_weather(self, obj):
show_weather = self.context.get('show_weather')
if show_weather:
lat = obj.location.y
lon = obj.location.x
return getWeatherFromLatLon(lat, lon)
# define default value if not show_weather in this
return ''
class Meta:
model = Camper
fields = ('camper_id', 'name', 'location', 'weather')
class CamperViewSet(viewsets.ModelViewSet):
...
def retrieve(self, request, *args, **kwargs):
"""Retrieve a Camper instance."""
instance = self.get_object()
show_weather = self.request.query_params.get('showWeather', False)
context = {
'show_weather': show_weather
}
serializer = CamperRetriveSerializer(instance, context=context)
return Response(serializer.data)
You can use two different serializers for this.
class CamperViewSet(viewsets.ModelViewSet):
serializer_class = CamperSerializer
def get_serializer_class(self):
serializer_class = self.serialzier_class
if self.request.method == 'GET':
serializer_class = CamperSerializerGet
return serializer_class
#Serializer for GET request
class CamperSerializerGet(serializers.ModelSerializer):
weather = serialziers.SerializerMethodField()
camper_id = serializers.IntegerField(source='id')
def get_weather(self, obj):
return obj.weather
class Meta:
model = Camper
fields = ('camper_id', 'name', 'location', 'weather')
#For other requests call this
class CamperSerializer(serializers.ModelSerializer):
camper_id = serializers.IntegerField(source='id')
class Meta:
model = Camper
fields = ('camper_id', 'name', 'location')

nested one to many serializer model django

I am trying to create a relationship between databases
result : =>
[
{
"id":1,
"title":"mobile",
"category_two":[
{
"id":3,
"title":"brand"
},
{
"id":4,
"title":"memory"
}
]
}
]
and i expect : =>
[
{
"id":1,
"title":"mobile",
"category_two":[
{
"id":3,
"title":"brand",
"category_three":[
{
"id":1,
"title":"samsung"
},
{
"id":2,
"title":"apple"
}
]
},
{
"id":4,
"title":"memory",
"category_three":[
{
"id":1,
"title":"32gb"
},
{
"id":2,
"title":"64gb"
}
]
}
]
}
]
// views
class get_Category(APIView):
def get(self, request):
category = CategoryOne.objects.all()
serializer = CategoryTwoSerializer(category, many=True)
return Response(serializer.data)
//serializers
class CategoryOneSerializer(serializers.ModelSerializer):
class Meta:
model = CategoryOne
fields = '__all__'
class CategoryTwoSerializer(serializers.ModelSerializer):
category_two= CategoryOneSerializer(many=True,read_only=True)
class Meta:
model = CategoryTwo
fields = '__all__'
depth=5
class CategoryThreeSerializer(serializers.ModelSerializer):
category_three = CategoryTwoSerializer(many=True,read_only=True)
class Meta:
model = CategoryThree
fields = '__all__'
depth=5
// models
class CategoryOne(models.Model):
title = models.CharField(max_length=225)
def __str__(self):
return self.title
class CategoryTwo(models.Model):
title = models.CharField(max_length=255)
categoryone = models.ForeignKey(CategoryOne,related_name='category_two',on_delete=models.SET_NULL,null=True)
def __str__(self):
return self.title
class CategoryThree(models.Model):
title = models.CharField(max_length=255)
categorytwo = models.ForeignKey(CategoryTwo,related_name='category_three',on_delete=models.SET_NULL,null=True)
def __str__(self):
return self.title
Try
class CategoryOneSerializer(serializers.ModelSerializer):
category_two= CategoryTwoSerializer(many=True,read_only=True)
class Meta:
model = CategoryOne
fields = '__all__'
class CategoryTwoSerializer(serializers.ModelSerializer):
category_three = CategoryThreeSerializer(many=True,read_only=True)
class Meta:
model = CategoryTwo
fields = '__all__'
class CategoryThreeSerializer(serializers.ModelSerializer):
class Meta:
model = CategoryThree
fields = '__all__'
// views
class get_Category(APIView):
def get(self, request):
category = CategoryOne.objects.prefetch_related("category_two", "category_two__category_three")
serializer = CategoryOneSerializer(category, many=True)
return Response(serializer.data)
If you want category_two rows related to a specific category_one to be serialized with it, you essentially want to add a non existing data to CategoryOne.
Since means you need two add it to the serializer and specify how to serialize it.
Here you can access this data by using a related_name, therefore adding a field with the proper related_name will suffice for django to find the data you want.
The same wan be done the serializer CategoryThree tied the a specific CategoryTwo row.
Howerver doing this will produce a LOT of SQL queries since each time my_category_on.category_two.all() is called by the serializer, an sql query is needed.
The prefetch_related() is here to solve the issue

Passing nested data to Django ModelSerializer

I'm wanting to know how you would pass nested data to a ModelSerializer if the child of the nested data is not a model on its own.
The data that I'm working with looks like this:
{
'leadId': 12345,
'updateTime': 1651250096821,
'changeInfo': {
'oldstage': 'New Leads',
'newstage': 'Attempting Contact'
}
}
From previous experience, I know that if I was only working with the leadId and the updateTime, my serializer would look like this:
class LogSerializer(serializers.ModelSerializer):
leadId = serializers.IntegerField(source="lead_id")
updateTime = serializers.IntegerField(source="update_time")
class Meta:
model = Log
fields = ["leadId", "updateTime"]
Which would then make it possible to do this:
data = {
'leadId': 12345,
'updateTime': 1651250096821
}
serializer = LogSerializer(data=data)
serializer.is_valid()
serializer.save()
If I'm not wanting to turn changeInfo into its own model, is it possible to map the fields to the nested data? Something that might look like this (but this obviously doesn't work):
class LogSerializer(serializers.ModelSerializer):
leadId = serializers.IntegerField(source="lead_id")
updateTime = serializers.IntegerField(source="update_time")
oldstage = serializers.IntegerField(source="oldstage")
newstage = serializers.IntegerField(source="newstage")
class Meta:
model = Log
fields = ["leadId", "updateTime", "oldstage", "newstage]
You can use a custom serializer for your changeInfo field (you don't need to create a model for that):
class ChangeInfoSerializer(serializers.Serializer):
oldstage = serializers.CharField(max_length=100, source="old_stage") # Set max_length to a value that suits your needs
newstage = serializers.CharField(max_length=100, source="new_stage")
def create(self, validated_data):
pass
def update(self, instance, validated_data):
pass
class LogSerializer(serializers.ModelSerializer):
leadId = serializers.IntegerField(source="lead_id")
updateTime = serializers.IntegerField(source="update_time")
changeInfo = ChangeInfoSerializer(required=False) # Change to required=True if you want this field to be mandatory
class Meta:
model = Log
fields = ["leadId", "updateTime", "changeInfo"]
def create(self, validated_data):
change_info = validated_data.pop('changeInfo')
for key, value in change_info.items():
if key == "old_stage":
validated_data['old_stage'] = value
elif key == "new_stage":
validated_data['new_stage'] = value
log = Log.objects.create(**validated_data)
return log
def update(self, instance, validated_data):
change_info = validated_data.pop('changeInfo')
instance.lead_id = validated_data.get('leadId', instance.lead_id)
instance.update_time = validated_data.get('updateTime', instance.update_time)
# Here you can use change_info['oldstage'] and change_info['newstage'] if 'changeInfo' is sent (otherwise you'll get a KeyError)
instance.save()
return instance
As mentioned in the comments, a SerializerMethodfield is a good way to go:
serializers.py
class LogSerializer(...):
...
changeInfo = serializers.SerializerMethodField()
def get_changeInfo(self, obj): return {
"leadId" : obj.lead_id,
"updateTime": obj.update_time
}
class Meta:
fields = ["changeInfo", ...]
...

Django REST Framework: Show only latest of nested object, return as un-nested JSON

What I'm trying to do in Django REST Framework: Return only latest nested object in list for an object and return it as JSON, with the sub-object un-nested.
My models:
class BaseObject(models.Model):
name = models.TextField()
object_type = models.ForeignKey(ObjectType)
class ObjectStatus(models.Model):
baseobject_id = models.ForeignKey('objects.BaseObject', related_name='status')
object_status = models.IntegerField()
object_status_timestamp = models.DateTimeField()
My serializers:
class ObjectStatusSimplifiedSerializer(serializers.ModelSerializer): #helper serializer to simplify status objects
class Meta:
model = ObjectStatus
fields = ['object_status', 'object_status_timestamp']
class ObjectStatusListSerializer(serializers.ModelSerializer): #request for last status of several objects
status = ObjectStatusSimplifiedSerializer(many=True)
class Meta:
model = BaseObject
fields = ['id', 'name', 'object_type', 'status']
My current view:
class ObjectStatusListView(generics.ListCreateAPIView):
serializer_class = ObjectStatusListSerializer
def get_queryset(self):
queryset = BaseObject.objects.all()
id = self.request.query_params.getlist('id')
if id:
queryset = queryset.filter(id__in=id)
return queryset
Current URL:
url(r'^objectstatus/status/list$', views.ObjectStatusListView.as_view()),
So now, when going to, for example, [...]/objectstatus/status/list?id=9, the result I get looks like this:
[
{
"id": 9,
"name": "r5",
"object_type": "router",
"status": [
{
"object_status": 1,
"object_status_timestamp": "2019-10-24T09:40:15.605391Z"
},
{
"object_status": 2,
"object_status_timestamp": "2019-10-24T09:40:28.133296Z"
},
{
"object_status": 3,
"object_status_timestamp": "2019-10-24T09:40:40.829486Z"
},
{
"object_status": 1,
"object_status_timestamp": "2019-10-24T09:40:53.333332Z"
}
]
}
]
What I want is to display only the object status with the most recent timestamp.
Also, I can't figure out how to flatten the JSON object, like this:
[
{
"id": 9,
"name": "r5",
"object_type": "router",
"object_status": 1,
"object_status_timestamp": "2019-10-24T09:40:53.333332Z"
}
]
With the following serializer, you should get the desired output. We filter the status list and get only the latest one and then we flatten the structure as you need.
class ObjectStatusListSerializer(serializers.ModelSerializer): #request for last status of several objects
status = serializers.SerializerMethodField(read_only=True)
class Meta:
model = BaseObject
fields = ['id', 'name', 'object_type', 'status']
def get_status(self, obj):
return ObjectStatusSimplifiedSerializer(instance=obj.status.order_by('object_status_timestamp').first()).data
def to_representation(self, obj):
"""Move fields from status to main object representation."""
representation = super().to_representation(obj)
status_representation = representation.pop('status')
for key in status_representation:
representation[key] = status_representation[key]
return representation
you can try change serializer to like this. I assum your ObjectType have field is name for line code object_type.name
class ObjectStatusSimplifiedSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField()
object_type = serializers.SerializerMethodField()
#staticmethod
def get_name(instance):
return instance.status.name
#staticmethod
def get_object_type(instance):
return instance.status.object_type.name
class Meta:
model = ObjectStatus
fields = ['id', 'name', 'object_type', 'object_status', 'object_status_timestamp']
class ObjectStatusListSerializer(serializers.ModelSerializer):
status = serializers.SerializerMethodField()
#staticmethod
def get_status(instance):
queryset = ObjectStatus.objects.filter(baseobject_id=instance).order_by('-object_status_timestamp')[:1]
if queryset.count():
return ObjectStatusSimplifiedSerializer(queryset, many=True).data
return []
class Meta:
model = BaseObject
fields = ['id', 'name', 'object_type', 'status']

How to include a parent object when serializing a Django Model?

I have two simple models with a foreign key relation like this:
class Foo(models.Model):
code = models.CharField(max_length=255)
class Bar(models.Model):
foo = models.ForeignKey(Foo)
description = models.CharField(max_length=255)
The current rest_framework-based serializers look like this:
class FooSerializer(serializers.ModelSerializer):
class Meta:
model = models.Foo
fields = ('id', 'code')
class BarSerializer(serializers.ModelSerializer):
class Meta:
model = models.Bar
fields = ('id', 'foo', 'description')
So a GET request for a Bar will return something like this:
{
"id": 1,
"foo": 2,
"description": "[…]"
}
How do I change BarSerializer to instead return the full Foo object, like this:
{
"id": 1,
"foo": {
"id": 2,
"code": "[…]"
},
"description": "[…]"
}
?
Keep in mind I still need to be able to create a Bar by providing only a description and Foo ID. I've tried various things including specifying foo = FooSerializer() in BarSerializer. The problem is that when I want to create a new Bar and link it to an existing Foo as before, it complains that I've not provided Foo's code property.
Simple and Elegant solution
override to_represention() method as,
class BarSerializer(serializers.ModelSerializer):
class Meta:
model = models.Bar
fields = ('id', 'foo', 'description')
def to_representation(self, instance):
data = super().to_representation(instance)
data['foo'] = FooSerializer(instance.foo).data
return data
Orginal version
use depth=1 in BarSerializer serializer
class BarSerializer(serializers.ModelSerializer):
class Meta:
model = models.Bar
fields = ('id', 'foo', 'description')
depth = 1
Reference
1. depth [DRF-Doc]
Update-1
Use Two different Serializers for Read and Write operations.
class BarWriteSerializer(serializers.ModelSerializer):
class Meta:
model = Bar
fields = ('id', 'foo', 'description')
def to_representation(self, instance):
data = super().to_representation(instance)
data['foo'] = FooSerializer(instance.foo).data
return data
class BarReadSerializer(serializers.ModelSerializer):
class Meta:
model = Bar
fields = ('id', 'foo', 'description')
depth = 1
and in your views, override the get_serializer_class() method as,
from rest_framework import viewsets
class SampleViewset(viewsets.ModelViewSet):
queryset = Bar.objects.all()
def get_serializer_class(self):
if self.action == 'create':
return BarWriteSerializer
return BarReadSerializer
The payload to use while Bar creation,
{
"foo":1,# The "pk" of "Foo" instance
"description":"bar description"
}
Solution:
class FooSerializer(serializers.ModelSerializer):
class Meta:
model = models.Foo
fields = ('id', 'code')
class BarSerializer(serializers.ModelSerializer):
foo = FooSerializer(models.Foo.objects.all(), read_only=True)
foo_id = serializers.PrimaryKeyRelatedField(
source='foo',
queryset=models.Foo.objects.all(),
write_only=True
)
class Meta:
model = models.Bar
fields = ('id', 'foo', 'foo_id', 'description')
Use a nested serializer as read_only to get the full Foo object.
Use a write_only field foo_id to use it for create/update.
Now, your request data will look like:
{ 'foo_id': 1, 'description': 'foo bar' }
Alternatively, if you dont want two fields, one for read and another for write, you can override the create/update methods of the serializer to capture the foo's id.
Example:
class BarSerializer(serializers.ModelSerializer):
foo = FooSerializer(Foo.objects.all())
class Meta:
model = models.Bar
fields = ('id', 'foo', 'description')
def create(self, validated_data):
foo = validated_data.pop('foo')
bar = Bar.objects.create(
foo=foo.id,
**validated_data
)
return bar
def update(self, instance, validated_data):
foo = validated_data.pop('foo')
instance.foo = foo
instance.update(
**validated_data
)
return instance
The request data, in this case, will be:
{ 'foo': {'id': '1', 'code': 'AAA'}, 'description': 'foo bar' }