Nested Serializer in Django Rest Framework - django

I am trying to make a nested serializer but when I run the following code it gives me an empty list. I tried to replicate the solution of this question and my problem is exactly similar
The only difference is in that answer serializer.Serializer is used but I am using Model Serializer
class hhhSerializer(serializers.Modelserializer):
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(read_only=True)
class Meta:
model = ItemBatch
fields = ('id','name')
class dispatchhistorySerializer(serializers.ModelSerializer):
truck_name = ReadOnlyField(source='truck_name.name')
truck_type = ReadOnlyField(source='truck_type.name')
items = hhhSerializer(many=True)
class Meta:
model = DispatchPlan
fields = "__all__"
Output:
"id": 35,
"truck_name": "24 ft mxl 14 Ton",
"truck_type": "Container",
"items": [
{},
{},
{},
{},
{},
{},
{},
{},
{},
{}
],

You have to declare the field explicitly at DispatchHistorySerializer.Meta.fields; now, as personal recommendation avoid always "all" in the field list
This code should work (I had renamed your classes to comform python convention)
class HhhSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(read_only=True)
class Meta:
model = ItemBatch
fields = ('id','name')
class DispatchHistorySerializer(serializers.ModelSerializer):
truck_name = ReadOnlyField(source='truck_name.name')
truck_type = ReadOnlyField(source='truck_type.name')
items = HhhSerializer(many=True) # 2) here we say how to serialize 'items'
class Meta:
model = DispatchPlan
fields = ('id', 'truck_name', 'truck_type', 'items',) # 1) here we say: include 'items' please
EDIT: if using ModelSerializer, define which model in the Meta class; if it isn't a ModelSerializer, use a simple Serializer instead

Related

Serializing many to many relationship with attributes does not show attribute in the relationship

My conceptual model is that there are DemanderFeature objects which have LoadCurve objects linked to them in a many-to-many relationship, along with a single attribute indicating "how many times" the two are associated, using an attribute in the many-to-many relationship called number.
I have been struggling for quite a while now, reading many answers on stackoverflow but I just cannot get it to work in exactly the way that I want. This is my desired output, when looking at the detail view of a DemanderFeature:
HTTP 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
[
{
"name": "testdemander",
"loadcurves": [
{"name": "lc", "number": 5},
{"name": "lc2", "number": 10}
],
// Other DemanderFeature fields removed for brevity...
}
]
The closest I have been able to get to this is with this setup:
Models
class LoadCurve(models.Model):
created = models.DateTimeField(auto_now_add=True)
finalized = models.BooleanField(default=False)
owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)
name = models.CharField(max_length=100)
length = models.IntegerField(default=0)
deleted = models.BooleanField(default=False)
demanderfeatures = models.ManyToManyField("DemanderFeature", through="DemanderFeatureToLoadCurveAssociation")
class Meta:
ordering = ['name']
constraints = [
models.UniqueConstraint(fields=["owner", "name"], condition=models.Q(deleted=False), name="loadcurve_unique_owner_name")
]
class DemanderFeature(models.Model):
created = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=100)
owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)
demanderfeaturecollection = models.ForeignKey(DemanderFeatureCollection, on_delete=models.CASCADE)
loadcurves = models.ManyToManyField("LoadCurve", through="DemanderFeatureToLoadCurveAssociation")
deleted = models.BooleanField(default=False)
geom = gis_models.PointField(default=None)
class Meta:
ordering = ['name']
constraints = [
models.UniqueConstraint(fields=["owner", "demanderfeaturecollection", "name"], condition=models.Q(deleted=False),
name="demanderfeature_unique_owner_demanderfeaturecollection_name")
]
class DemanderFeatureToLoadCurveAssociation(models.Model):
loadcurve = models.ForeignKey(LoadCurve, on_delete=models.CASCADE)
demanderfeature = models.ForeignKey(DemanderFeature, on_delete=models.CASCADE)
number = models.IntegerField()
Serializers
(I am using __all__ for the sake of debugging, so that I can see everything that is being serialized and available)
class LoadCurveSerializer(serializers.ModelSerializer):
class Meta:
model = LoadCurve
fields = "__all__"
class DemanderFeatureToLoadCurveAssociationSerializer(serializers.ModelSerializer):
class Meta:
model = DemanderFeatureToLoadCurveAssociation
fields = "__all__"
class DemanderFeatureSerializer(serializers.ModelSerializer):
demanderfeaturecollection = serializers.SlugRelatedField(slug_field="name", queryset=DemanderFeatureCollection.objects.all())
loadcurves = LoadCurveSerializer(read_only=True, many=True)
# loadcurves = DemanderFeatureToLoadCurveAssociationSerializer(read_only=True, many=True)
class Meta:
model = DemanderFeature
fields = "__all__"
lookup_field = "name"
There is a commented line in the previous code block which I was trying to use to get the DemanderFeatureToLoadCurveAssociationSerializer because I thought this would be the proper way to get the number field which its related model defines, but when I uncomment this line (and comment the line just below it) I only get this error:
AttributeError at /demanderfeatures/
Got AttributeError when attempting to get a value for field `number` on serializer `DemanderFeatureToLoadCurveAssociationSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `LoadCurve` instance.
Original exception text was: 'LoadCurve' object has no attribute 'number'.
If I do not swap those lines, however, I get this as a result:
HTTP 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
[
{
"name": "testdemander",
"loadcurves": [
{
"id": 1,
"created": "2020-12-29T11:29:11.585034Z",
"finalized": true,
"name": "lc",
"length": 0,
"deleted": false,
"owner": 1,
"demanderfeatures": [
1
]
},
{
"id": 2,
"created": "2020-12-29T12:46:31.044624Z",
"finalized": true,
"name": "lc2",
"length": 0,
"deleted": false,
"owner": 1,
"demanderfeatures": [
1
]
}
],
// Other DemanderFeature fields removed for brevity...
}
]
Which does not contain that critical number field which is defined in the DemanderFeatureToLoadCurveAssociation model.
I feel like I am just missing something quite obvious but I have not been able to find it.
I'm putting my specific implementation here in case it is useful to anyone. It is adapted from #JPG's own Q&A which was made as a response to this question (probably because it would be a pain to try to re-create my entire ORM and environment). I adapted their code to my own implementation.
It is worth noting however that this implementation only works one way. I originally tried implementing it with such a conceptual mapping:
DemanderFeature -> Person
LoadCurve -> Group
Membership -> DemanderFeatureToLoadCurveAssociation
But since I wanted to see all the associated LoadCurve objects when retrieving a DemanderFeature object, I actually needed to flip this mapping around so that DemanderFeature objects are equivalent to Group objects and LoadCurve objects are equivalent to Person objects. It is obvious to me now why this is (I am conceptualizing the DemanderFeature as a GROUP of LoadCurves), but at the time I didn't see the technical difference.
Anyway here is the updated serializers.py. Thanks again #JPG for taking the time!
class LoadCurveSerializer(serializers.ModelSerializer):
class Meta:
model = LoadCurve
fields = ["name"]
def serialize_demanderfeaturetoloadcurveassociation(self, loadcurve_instance):
# simple method to serialize the through model fields
demanderfeaturetoloadcurveassociation_instance = loadcurve_instance \
.demanderfeaturetoloadcurveassociation_set \
.filter(demanderfeature=self.context["demanderfeature_instance"]) \
.first()
if demanderfeaturetoloadcurveassociation_instance:
return DemanderFeatureToLoadCurveAssociationSerializer(demanderfeaturetoloadcurveassociation_instance).data
return {}
def to_representation(self, instance):
rep = super().to_representation(instance)
return {**rep, **self.serialize_demanderfeaturetoloadcurveassociation(instance)}
class DemanderFeatureToLoadCurveAssociationSerializer(serializers.ModelSerializer): # create new serializer to serialize the through model fields
class Meta:
model = DemanderFeatureToLoadCurveAssociation
fields = ["number"]
class DemanderFeatureSerializer(serializers.ModelSerializer):
demanderfeaturecollection = serializers.SlugRelatedField(slug_field="name", queryset=DemanderFeatureCollection.objects.all())
loadcurves = serializers.SerializerMethodField()
class Meta:
model = DemanderFeature
fields = ["name", "demanderfeaturecollection", "loadcurves", "geom"]
# fields = "__all__"
lookup_field = "name"
def get_loadcurves(self, demanderfeature):
return LoadCurveSerializer(
demanderfeature.loadcurve_set.all(),
many=True,
context={"demanderfeature_instance": demanderfeature} # should pass this `group` instance as context variable for filtering
).data

Create a field with conditions

I'm a beginner in django rest framework.
I am wondring if there is a way to create a field from a GET. For example if "count" == 0, create a field named "available" : "out_of_stock" else "available
models.py
class Count(models.Model):
name = models.CharField(max_length=100)
count = models.IntergerField()
serializers.py
class CountSerializer(serializers.ModelSerializer):
class Meta:
model = Count
fields = '__all__'
views.py
class CountViewSet(viewsets.ModelViewSet):
queryset = Count.objects.all()
serializer_class = CountSerializer
output
[
{
"id": 1,
"count": 10,
},
{
"id": 2,
"count": 0,
}
]
First, for a good practise, avoid using fields = '__all__' and instead define your fields specifically.
For ex: fields = ['name', 'count'], you will see doing so will pay off shortly as I continue.
DRF has the feature to mark a serializer field as read_only or write_only so it works on specific requests, as it looks from the naming read_only intended for GET and write_only for POST.
You can do what you are looking for in so many ways, but I think the easiest way to do it would be using SerializerMethodField like this:
class CountSerializer(serializers.ModelSerializer):
available = serializers.SerializerMethodField()
def get_available((self, obj):
value = 'out_of_stock' if obj.count == 0 else 'available'
return value
class Meta:
model = Count
fields = ['name', 'count', 'available']
For more advanced needs, you can read about dynamic serializer fields on drf docs, see.

How to make custom response in djangorestframework

So I like the idea of using class-based views and ModelSerializers but I have an issue with it for my particular use case. Maybe I am not using it as it's intended to be used.
class CarSerializer(serializers.ModelSerializer):
class Meta:
model = CarModel
fields = ['car_name']
# A car can have build for multiple years
class MakelHistorySerializer(serializers.ModelSerializer):
car = CarSerializer(many=True, read_only=True)
class Meta:
model = MakeHistoryModel
fields = ['model_year', 'car']
The response is:
{
"car": {
"car_name": "Fiesta"
},
"model_year": "2020"
}
My two model classes, CarModel and MakeHistoryModel have ["id", "car_name", "manufacturer"] and ["id", "car_id", "model_year", "country_id"] fields respectively.
What kind of a response I really want is:
{
"car_name": "Fiesta",
"model_year": "2020"
}
How would I do this?
You don't need to first serializer (CarSerializer).Just this serializer which has SerializerMethodField enough for your output:
class MakelHistorySerializer(serializers.ModelSerializer):
car_name = serializers.SerializerMethodField()
class Meta:
model = MakeHistoryModel
fields = ['model_year', 'car_name']
def get_car_name(self,obj):
return obj.car.name if obj.car_id else ''
# I don't know your model so to avoid NoneType error, I added this check

insertion in nested serializer in DRF

Currently i am using django rest_framework.I have two different class name as Customer and Customerinfo . My code is working properely . Now I want to insert value in the customer serializer. In CustomerSerializer it has Customerinfo field.
These are the serializer:
class CustomerinfoSerializer(serializers.ModelSerializer):
class Meta:
model = Choice
fields = ['id','phone']
class CustomerSerializer(serializers.ModelSerializer):
info = CustomerinfoSerializer(many=True, source='customerinfo_set')
class Meta:
model = Question
fields = ["id","name","info"]
How can i use post method to insert value ? sample values are:
{
"name": "user10",
"info":
[
{ "phone":12345
},
{ "phone":54321
}
]
}
If I understand you correctly your looking for writable nested serializers.
As documentation says,
by default nested serializers are read-only. If you want to support
write-operations to a nested serializer field you'll need to create
create() method.
You could try something similar to this (but you need to adjust your naming):
class CustomerSerializer(serializers.ModelSerializer):
info = CustomerinfoSerializer(many=True, source='customerinfo_set')
class Meta:
model = Question
fields = ["id", "name", "info"]
def create(self, validated_data):
info_data = validated_data.pop('info')
question = Question.objects.create(**validated_data)
for info in info_data:
Choice.objects.create(question=question, **info)
return question

Append "_id" to foreign key fields in Django Rest Framework

I have a model like so:
class MyModel(models.Model):
thing = models.ForeignKey('Thing')
Serializers and ViewSet like so:
class ThingSerializer(serializers.ModelSerializer):
class Meta:
model = Thing
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
For MyModel list endpoint, DRF return objects like:
[
{ id: 1, thing: 1 },
{ id: 2, thing: 1 },
{ id: 3, thing: 2 },
{ id: 4, thing: 4 }
]
Is there a way to tell DRF to automatically include "_id" on the end of ForeignKey fields that are just IDs and not the actual related object? e.g.
[
{ id: 1, thing_id: 1 },
{ id: 2, thing_id: 1 },
{ id: 3, thing_id: 2 },
{ id: 4, thing_id: 4 }
]
Found same request and solution here:
https://github.com/tomchristie/django-rest-framework/issues/3121
https://gist.github.com/ostcar/eb78515a41ab41d1755b
The AppendIdSerializerMixin.get_fields override suffices for output of JSON objects (with _id appended) but when writing back to the API, it's a little more complicated and the logic in IdPrimaryKeyRelatedField and IdManyRelatedField handles that.
class IdManyRelatedField(relations.ManyRelatedField):
field_name_suffix = '_ids'
def bind(self, field_name, parent):
self.source = field_name[:-len(self.field_name_suffix)]
super(IdManyRelatedField, self).bind(field_name, parent)
class IdPrimaryKeyRelatedField(relations.PrimaryKeyRelatedField):
"""
Field that the field name to FIELD_NAME_id.
Only works together the our ModelSerializer.
"""
many_related_field_class = IdManyRelatedField
field_name_suffix = '_id'
def bind(self, field_name, parent):
"""
Called when the field is bound to the serializer.
Changes the source so that the original field name is used (removes
the _id suffix).
"""
if field_name:
self.source = field_name[:-len(self.field_name_suffix)]
super(IdPrimaryKeyRelatedField, self).bind(field_name, parent)
class AppendIdSerializerMixin(object):
'''
Append '_id' to FK field names
https://gist.github.com/ostcar/eb78515a41ab41d1755b
'''
serializer_related_field = IdPrimaryKeyRelatedField
def get_fields(self):
fields = super(AppendIdSerializerMixin, self).get_fields()
new_fields = type(fields)()
for field_name, field in fields.items():
if getattr(field, 'field_name_suffix', None):
field_name += field.field_name_suffix
new_fields[field_name] = field
return new_fields
class MyModelSerializer(AppendIdSerializerMixin, serializers.ModelSerializer):
class Meta:
model = MyModel
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
This has worked for me as of today in July 2022.
def ParentModel():
...
def ChildModel():
parent = models.ForeignKey(ParentModel)
serializers.py
def ChildSerializer():
parent_id = serializers.PrimaryKeyRelatedField(
source="parent",
queryset=ParentModel.objects.all(),
)
class Meta:
exclude = ["parent"]
fields = "__all__"
The source="parent" in the serializer field, is what allows this rename to happen; it needs to match the ForeignKey field name in ChildModel.
Note: The exclude = ["parent"] is required if you are using fields = "__all__" so that DRF doesn't require or return the default field name.
Note 2: If using UUID for your ID field then you'll need to add pk_field=UUIDField(format="hex_verbose"), to the serializer field you're renaming to {field}_id
You can use db_column model field option in your model:
class MyModel(models.Model):
thing = models.ForeignKey('Thing', db_column='thing_id')
Or if you don't want to change your model, you can do so by changing source serializer field in your serializer:
class ThingSerializer(serializers.ModelSerializer):
thing_id = serializers.IntegerField(source='thing')
class Meta:
model = Thing
fields = ('thing_id','other_field', 'another_field')
ok matt simply add that parameter to your model class:
thing = models.ForeignKey(Thing, related_name='thing_id')
It appears to be rather complicated since it's not something DRF allows to configure. But like always, you can override things.
Everything seems to happens in the model_meta.py file. In this file, you can replace
forward_relations[field.name] = RelationInfo(
by
forward_relations[field.name + '_id'] = RelationInfo(
Careful, you need to do it twice in that function.
Once you've done that, you have still work to do as the ModelSeralizer depends on the real model_meta. It appears you need to replace those three lines:
https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/serializers.py#L865
https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/serializers.py#L944
https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/serializers.py#L1425
Then, you need to implement a MyModelSerializer which overrides ModelSerializer and the three methods: create, get_fields, get_unique_together_validators. I tested it on GET requests and it works.
As you can see, it's a significant amount of code rewriting which implies difficulties for maintaining upgrades. Then, I would strongly recommend to think twice before doing so. In the mean time, you can still open an issue on the DRF project for making it more configurable (and maintainable).