How do I write to nested parent in Django Serializer - django

I'm using Django serializers to create an API which I both read from and write to.
Models:
class Biomarker(models.Model):
low_value_description = models.TextField(blank=True, null=True)
high_value_description = models.TextField(blank=True, null=True)
class BiomarkerReading(models.Model):
biomarker = models.ForeignKey(Biomarker, on_delete=models.CASCADE)
test = models.ForeignKey(Test, on_delete=models.CASCADE)
value = models.DecimalField(max_digits=30, decimal_places=8, default=0)
Serializer:
class BiomarkerReadingSerializer(serializers.ModelSerializer):
class Meta:
model = BiomarkerReading
fields = (
'id', 'test', 'biomarker', 'value'
)
JSON format:
{
"id": 617188,
"test" 71829,
"biomarker": 32,
"value": 0.001
}
The above all works, and I can read and write to it with that JSON format. However I now need to add some fields from the parent model so the response looks like this:
{
"id": 617188,
"test" 71829,
"biomarker": {
"id": 32,
"low_value_description": "All good",
"high_value_description": "You will die",
},
"value": 0.001
}
I have got the read part working using these Serializers:
class BiomarkerDescriptionSerializer(serializers.ModelSerializer):
class Meta:
model = Biomarker
fields = ('id', 'low_value_description', 'high_value_description')
class BiomarkerReadingSerializer(serializers.ModelSerializer):
biomarker = BiomarkerDescriptionSerializer()
class Meta:
model = BiomarkerReading
fields = (
'id', 'test', 'biomarker', 'value'
)
However I can't find a way to write to it using the old json format (with "biomarker": 32 in the JSON).
I thought I would need to do something in validate or create, but I get a 400 response before it even hits those methods:
class BiomarkerReadingSerializer(serializers.ModelSerializer):
... # as above
def validate(self, data):
print('validate') # Doesn't print
data = super().validate(data)
return data
def create(self, validated_data):
print('create') # Doesn't print
return super().create(validated_data)
The example in the docs for writable-nested-serializers and the other examples I've found on SO only discuss the case of creating child records while writing to the parent record serializer, not the other way around.
I do not want to create a parent Biomarker via the API, I just need to be able to reference it by pk/id in the incoming JSON like before.
I don't mind if I have to change the names of keys to something like this for incoming:
{
"id": 617188,
"test" 71829,
"biomarker_id": 32,
"value": 0.001
}
Or something like this for the response:
{
"id": 617188,
"test" 71829,
"biomarker": 32,
"descriptions": {
"low_value_description": "All good",
"high_value_description": "You will die",
},
"value": 0.001
}
If that makes it easier.

I hate answering my own question, but the solution was to subclass serializers.RelatedField, which is explained in advanced-serializer-usage).
This lets you separately control how the value(instance) is represented as serialised, and how the serialised data is used to retrieve or create a value (instance).
class BiomarkerDescriptionSerializer(serializers.RelatedField):
def to_representation(self, value):
data = {}
for field in ('id', 'low_value_description', 'high_value_description'):
data[field] = getattr(value, field)
return data
def to_internal_value(self, data):
return Biomarker.objects.get(id=data)
def get_queryset(self, *args):
pass
class BiomarkerReadingSerializer(serializers.ModelSerializer):
biomarker = BiomarkerDescriptionSerializer()
...
The overridden get_queryset is required, even though it does nothing.
This means data going in looks like this:
{
"id": 617188,
"test" 71829,
"biomarker": 32,
"value": 0.001
}
Yet the data going out looks like this:
{
"id": 617188,
"test" 71829,
"biomarker": {
"id": 32,
"low_value_description": "All good",
"high_value_description": "You will die",
},
"value": 0.001
}
Thank you to those who offered answers, much appreciated

Using depth = 1 option will solve your problem.
class BiomarkerReadingSerializer(serializers.ModelSerializer):
class Meta:
model = BiomarkerReading
fields = (
'id', 'test', 'biomarker', 'value'
)
depth = 1
Take a look: https://www.django-rest-framework.org/api-guide/serializers/#specifying-nested-serialization

Related

Use to_representation to combine fields from related models in Django Rest Framework

I've got the following models:-
class Player(models.Model):
first_name = models.CharField()
etc.
class Game(models.Model):
date = models.DateField()
etc.
class GameMembership(models.Model):
player = models.ForeignKey(Player, related_name="memberships")
game = models.ForeignKey(Game, related_name="memberships")
available = models.BooleanField(default=False)
I've created a ModelViewSet to return all the Players but I'd like to be able to, for each player in the list, return a list of their game memberships. I can do that, quite easily, but the data it returns looks like this:-
{
"id": "1",
"memberships": [
{
"available": True,
"game": {
"date": "a date",
etc.
}
},
{
"available": False,
"game": {
"date": "a date",
etc.
}
}
]
}
but I'd like to hide the "memberships" aspect of my database from the API users and return something like this instead:-
{
"id": "1",
"games": [
{
"available": True,
"date": "a date",
etc.
},
{
"available": False,
"date": "a date",
etc.
}
]
},
So I want to take a field (or two) from the GameMembership model and combine it with all the fields from the Game model but crucially, I want it all in to one dictionary in the returned results. I know I can simply serialize Game on the GameMembershipSerializer, but that means that I'll be returning:-
{
"id": "1",
"games": [
{
"available": True,
"game": {
"date": "a date",
etc.
}
},
{
"available": False,
"game": {
"date": "a date",
etc.
}
}
]
}
which doesn't really make sense, as the user will have to access things like results['games'][1]['game'] which seems wrong.
I thought I could do it by using to_representation on the GameMembershipSerializer but I can't figure it out.
Any ideas?
Another approach without overriding to_representation, is to use a SerializerMethodField and do the processing there like this:
class GameModelSerializer(models.ModelSerializer):
class Meta:
model = Game
fields = '__all__'
class PlayerModelSerializer(models.ModelSerializer):
games = serializers.SerializerMethodField()
class Meta:
model = Player
fields = ('id', 'games')
def get_games(self, player)
return [{
'available': membership.available,
**GameModelSerializer(membership.game).data,
} for membership in player.memberships.all()]
And to make sure that the serializer doesn't hit the db per membership just to get the game, you can prefetch the membership with the game selected like this:
Player.objects.prefetch_related(
Prefetch('memberships', queryset=GameMembership.objects.select_related('game'))
)
EDIT:
You could also do:
def get_games(self, player)
return [{
**GameMembershipSerializer(membership).data,
**GameModelSerializer(membership.game).data,
} for membership in player.memberships.all()]
if you want to reuse GameMembershipSerializer.
For those that are interested in how I ended up solving this, I needed to use to_representation as it needed to be an expandable field (using drf-flex-fields). I also found that the following implementation was a lot quicker than the code mentioned above so I used this instead:-
# PlayerSerializer
games = GameMembershipGameSerializer(many=True)
class GameMembershipGameSerializer(serializers.ModelSerializer):
class Meta:
model = GameMembership
fields = ["available", "game"]
def to_representation(self, membership):
representation = super().to_representation(membership)
game = representation.pop("game")
for key in game:
representation[key] = game[key]
return representation

IntegrityError in djnago

Can't find a solution to this error please help or show me how to fix this.
The error is showing as follows:
(1048, "Column 'targetDefn_id' cannot be null") this error is showing when I post data.
This is my views.py in Post method I m getting this error:
def setTarget(request):
if request.method == 'POST':
data=JSONParser().parse(request)
print(data)
serial=TargetSerializers(data=data)
print(serial)
if serial.is_valid():
serial.save()
print("done")
return JsonResponse(serial.data,status=status.HTTP_200_OK,safe=False)
return JsonResponse(serial.errors, status=status.HTTP_400_BAD_REQUEST)
This is my Serializer.py as follows:
class TargetSerializers(serializers.ModelSerializer):
targetDefn=serializers.SerializerMethodField()
roleId=serializers.SerializerMethodField()
empId=serializers.SerializerMethodField()
class Meta:
model = Target
fields = (
'id',
'targetDefn',
'roleId',
'empId',
'startDate',
'endDate',
'value'
)
def get_targetDefn(self,obj):
trgt = TargetDefination.objects.get(id=obj.targetDefn_id)
serial = TargetDefinationSerializers(trgt)
return serial.data
def get_empId(self,obj):
emp= Employee.objects.get(id=obj.empId_id)
serial= OnlyEmployeeSerializers(emp)
return serial.data
def get_roleId(self,obj):
role=Role.objects.get(id=obj.roleId_id)
serial=RoleSerializers(role)
return serial.data
This is models.py as follows:
class Target(models.Model):
targetDefn=models.ForeignKey(TargetDefination,on_delete=models.CASCADE)
roleId=models.ForeignKey(Role,on_delete=models.CASCADE)
empId=models.ForeignKey(Employee,on_delete=models.CASCADE)
startDate= models.DateField(default=datetime.date.today)
endDate= models.DateField(null=True,blank=True)
value=models.PositiveIntegerField(default=0)
def __str__(self):
return str(self.empId) + ' ' +str(self.targetDefn)
Updated:
My Post query:
{
"targetDefn": {
"id": 1,
"targetName": "MIN SALES",
"displayName": "MIN SALES"
},
"roleId": {
"id": 3,
"roleName": "CIO",
"description": "chief information officer",
"roleReportsTo": 5,
"roleReportsToName": "SeniorVP(M)"
},
"empId": {
"id": 2,
"empName": "Emp02",
"startDate": "2021-04-01",
"termDate": null
},
"startDate": "2021-05-11",
"endDate": "2021-05-20",
"value": "123"
}
Know that the SerializerMethodField is READ-ONLY. It means that the data of those fields will not be written in the database. They will be computed and sent to your user, but never written in the DB. So, when you're saving your serializer, those fields are ignored.
I've noticed that you've created SerializerMethodField for all your foreign keys, only for their get_[field] method to return literally the serialized object. So why not simply use their nested-serializer right away?
class TargetSerializers(serializers.ModelSerializer):
targetDefn=TargetDefinationSerializers()
roleId=OnlyEmployeeSerializers()
empId=serializers.SerializerMethodField()
class Meta:
model = Target
fields = (
'id',
'targetDefn',
'roleId',
'empId',
'startDate',
'endDate',
'value'
)
If you cannot get it to work or wish to keep your SerializerMethodField structure, then we must add the data of those 3 fields before saving. You can either pass the data as parameter in the serializer, like so:
serializer.save(targetDefn=variable_1, roleId=variable_2, empId=variable_3)
Or you can override the create and update serializer methods to do it there, example:
def create(self, validated_data):
targetDefn = validated_data["targetDefn"]
roleId = validated_data["roleId"]
emp_id = validated_data["emp_id"]
# preprocess those data if needed
# Then create your model
Target.objects.create(targetDefn=targetDefn, ...)

How to change default behavior of serializers

I am doing some stuff with the django restframework. Very basic now, but I like to have my data coming back to me bit differently.
This is how I get the response now:
[
{
"line": "line1",
"user_text": "Some text",
"topic": "value/xrp"
},
{
"line": "line2",
"user_text": "Some text 2",
"topic": "beer/heineken/sale"
}
]
This is what I like to get:
{
"line1": {
"user_text": "Some text",
"topic": "value/xrp"
},
"line2": {
"user_text": "Some text 2",
"topic": "beer/heineken/sale"
}
}
This is my serializer:
class LineSerializer(serializers.Serializer):
line = serializers.CharField(max_length=16)
user_text = serializers.CharField(max_length=16)
topic = serializers.CharField()
This is the view
class DisplayDetailAPIView(ListAPIView):
serializer_class = LineSerializer
def get_queryset(self):
return Line.objects.filter(display__serial_number=self.kwargs['serial_number'])
And the model (as reference)
class Line(models.Model):
display = models.ForeignKey(Display, on_delete=models.CASCADE, related_name='lines')
line = models.CharField(max_length=16)
user_text = models.CharField(max_length=16, null=True, blank=True)
topic = models.ForeignKey(Topic, on_delete=models.CASCADE, blank=True, null=True)
def __str__(self):
return self.line
I looked in the rest framework documentation and a bit here on stackoverflow but I could not find an answer yet.
If someone has hints for me that would be very much appreciated :)
You can override to_representation in your serializer class:
class LineSerializer(serializers.ModelSerializer):
class Meta:
model = Line
fields = ('user_text', 'topic')
def to_representation(self, instance):
data = super().to_representation(instance)
response_data = {}
response_data[instance.line] = data
return response_data
You just create a temporary data dictionary and assign the current instance line attribute to be the key of your JSON response - what you actually want to have in the end:
[
{
"line1": {
"user_text": "lorem ipsum",
"topic": ...
}
},
{
"line2": {
"user_text": "dolor sin amet",
"topic": ...
}
}
]
Note 1: You may want to use a ModelSerializer instead of Serializer in this particular case, I think it is more straightforward to use and you can access instance.line directly, without specifying it actually to be serialized.
Note 2: Here your topic field will be serialized as an id since it is a ForeignKey relation on a database level. If you want to serialize topic as a CharField from one of the actual topic's fields, have a look here.

How to post array data through serializers?

I want to post some array data through serializer's create method. How do I get array data in serializer's create method?
this is my result which gives error because of array's data. I have to post belows particular,inch_first...rate arrays data.
{
"order_media": {
"client_employee": "4",
"client": "63",
"narration": "Print ad",
"vendor": "68",
"total_amount": "2590.00",
"discount_rate_client": "10.00",
"discount_amount_client": "259.00",
"service_percentage": "10.00",
"service_amount": "259.00",
"client_vat": "388.50",
"total_receivable": "2978.50",
},
"pub_date": "2019-04-03",
"particular": ["Banner", "Poster", "Plastic banner"],
"inch_first": ["4", "5", "3"],
"inch_second": ["4", "5", "3"],
"size": ["16.00", "25.00", "9.00"],
"quantity": ["5", "5", "6"],
"rate": ["10", "10", "10"],
}
This is my model
class RequestPrintProduction(models.Model):
particular = models.CharField(max_length=255,null=True,blank=True)
inch_first = models.CharField(max_length=45,null=True,blank=True)
inch_second = models.CharField(max_length=45,null=True,blank=True)
size = models.CharField(max_length=45,blank=True, null=True)
quantity = models.CharField(max_length=45,null=True, blank=True)
rate = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal(0.00), null=True, blank=True)
pub_date = models.DateField(blank=True, null=True)
vendor = models.ForeignKey(Party, blank=True, null=True)
request_id = models.ForeignKey(Request, blank=True, null=True, related_name='requestprint')
def __str__(self):
return self.particular
this is my api views:
class PrintRequestAPIView(ListBulkCreateAPIView):
serializer_class = RequestPrintSerializer
this is my serializer:
class RequestPrintSerializer(serializers.ModelSerializer):
order_media = OrderMediaSerializer(required=False)
class Meta:
model = RequestPrintProduction
fields = '__all__'
def create(self, validated_data):
service_request = Request()
service_request.save()
validated_data['request_id'] = service_request
order_media = validated_data.pop('order_media')
print(order_media)
online_service_request = self.Meta.model.objects.create(**data)
order_media['request_id'] = service_request
order_media = OrderMedia.objects.create(**order_media)
return online_service_request
I expect to post array's data successfully.
Let's try to arrange the post data like below. And then whenever you are initializing RequestPrintSerializer for this request initialize the serializer with many=True. Hope it helps. Good lucks.
{
"order_media": {
"client_employee": "4",
"client": "63",
"narration": "Print ad",
"vendor": "68",
"total_amount": "2590.00",
"discount_rate_client": "10.00",
"discount_amount_client": "259.00",
"service_percentage": "10.00",
"service_amount": "259.00",
"client_vat": "388.50",
"total_receivable": "2978.50"
},
"request_print_production": [
{
"pub_date" : "2019-04-03",
"particular": "Banner",
"inch_first": "4",
"inch_second": "4",
"size": "16.00",
"quantity": "5",
"rate": "10"
},
{
"pub_date" : "2019-04-03",
"particular": "Poster",
"inch_first": "5",
"inch_second": "5",
"size": "25.00",
"quantity": "5",
"rate": "10"
},
{
"pub_date" : "2019-04-03",
"particular": "Plastic Banner",
"inch_first": "3",
"inch_second": "3",
"size": "9.00",
"quantity": "6",
"rate": "10"
}
]
}
It seems like you have a char field in your database, but want to post an array to DRF. In fact, as the comments mentioned, this is an issue with nearly all your fields. You seem to get around this by just passing strings in everywhere.
Without database support this won't work directly. However, if you do really want to do this then you are going to have to do some of the work in your own code. There are custom fields for this out there somewhere, probably, but I've never seen 'em.
JSON Post Only
Here's an easy way to go about doing that:
Declare the field on your serializer directly as a ListField
In the 'validate' function, convert to a string (csv, json.dumps, etc)
Note that I'm not checking for empty values, settings defaults, etc. Please read the documentation (and source) for ListField on your own.
class Serializer(ModelSerializer):
particular = ListField(child=CharField(max_length=10))
def validate_particular(self, value):
"""Convert the list to a json string, and do extra validation if needed"""
return json.dumps(value)
Input & Output (Post & Get)
If you are using this both for input and output, and want to return a list from your model, you are going to need to make a custom field:
class ListAsJsonField(fields.ListField):
"""
Converts an incoming list of string values to json.
This field is _very_ primitive, lots of edge cases may break it.
"""
child = fields.CharField(min_length=1) # always use a char field
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def to_representation(self, data):
"""Convert to our output representation. Assumes input is always valid json now"""
return json.loads(data)
def to_internal_value(self, data):
"""Convert to the internal value -> database (and validated_data)"""
return json.dumps(data)
class MySerializer(serializers.ModelSerializer):
particular = ListAsJsonField(required=True, min_length=1)
...
Notes:
Some databases like postgres have a custom JSON field & Array field. Django ships with fields for those in django.contrib.postgres. Using these would imply you also want to fix your other model issues.
What the heck is ListBulkCreateAPIView?

Django - Retrieve nested fields multiple levels deep using foreign keys

I'm struggling to write a Django GET that returns the following looking response:
{
"lists": [
{
"id": "123",
"list_order": [
{
"id": "123_1",
"order": 1,
"list_id": "123",
"item_id": 9876,
"item": {
"id": 9876,
"name": "item1",
"location": "California"
}
},
{
"id": "123_2",
"order": 2,
"list_id": "123",
"item_id": 2484,
"item": {
"id": 2484,
"name": "item2",
"location": "California"
}
}
],
"updated_date": "2018-03-15T00:00:00Z"
}
]
}
Given a list_id, the response returns the basic information on the list ("id", "updated_date"), as well as the order of items in the list. Inside each item in the list order, it also grabs the related item details (nested in "item"). I'm able to get this response without the "item" details ("id", "name", "location" fields) and with no error:
{
"lists": [
{
"id": "123",
"list_order": [
{
"id": "123_1",
"order": 1,
"list_id": "123",
"item_id": 9876
},
{
"id": "123_2",
"order": 2,
"list_id": "123",
"item_id": 2484
}
],
"updated_date": "2018-03-15T00:00:00Z"
}
]
}
Again there is no error, and I can retrieve the first nested level without any issue. The problem is getting the "item" information to show within each "list_order". Below are my models, serializers, and views.
models.py
class Lists(models.Model):
id = models.CharField(null=False, primary_key=True, max_length=900)
updated_date = models.DateTimeField(blank=True, null=True)
class Meta:
managed = False
db_table = 'tbl_lists'
class Items(models.Model):
id = models.BigIntegerField(primary_key=True)
name = models.TextField(blank=True, null=True)
location = models.TextField(blank=True, null=True)
class Meta:
managed = False
db_table = 'tbl_items'
class ListOrder(models.Model):
id = models.CharField(null=False, primary_key=True, max_length=900)
list_id = models.ForeignKey(Lists, db_column='list_id', related_name='list_order')
item_id = models.ForeignKey(Items, db_column='item_id', related_name='items')
order = models.BigIntegerField(blank=True, null=True)
class Meta:
managed = False
db_table = 'tbl_list_order'
serializers.py
class ItemsSerializer(serializers.ModelSerializer):
class Meta:
model = Items
fields = '__all__'
class ListOrderSerializer(serializers.ModelSerializer):
item = ItemsSerializer(many=False, read_only=True)
class Meta:
model = ListOrder
fields = '__all__'
class ListsSerializer(serializers.ModelSerializer):
list_order = ListOrderSerializer(many=True, read_only=True)
class Meta:
model = Lists
fields = '__all__'
views.py
class ListsViewSet(generics.ListCreateAPIView):
"""
API endpoint that returns a list with its meta-information
"""
queryset = Lists.objects.all()
serializer_class = ListsSerializer
def get_queryset(self):
list_id = self.kwargs['list_id']
filters = [Q(id=list_id)]
return Lists.objects.filter(*filters)
def list(self, request, list_id):
queryset = self.get_queryset()
list_serializer = ListsSerializer(queryset, many=True)
return Response({ 'lists': list_serializer.data })
I'm pretty new to Django and like what it offers so far, though maybe I'm thinking of doing this in too much of a "SQL" way. I've read about select_related() and prefetch_related(), but not sure how I would apply it to this case. Any assistance is greatly appreciated and let me know if there's any other information I can provide.
In your ListOrderSerializer you are trying to serialize item. while in ListOrder model you used the field name item_id
Solution:
In ListOrderSerializer:
class ListOrderSerializer(serializers.ModelSerializer):
item_id = ItemsSerializer(many=False, read_only=True)
...