I have the following models
class Breed(models.Model)::
name = models.CharField(max_length=200)
class Pet(models.Model):
owner = models.ForeignKey(
"User",
on_delete=models.CASCADE,
)
name = models.CharField(max_length=200)
breed = models.ForeignKey(
"Breed",
on_delete=models.CASCADE,
)
I am trying to add few fileds for representation
purpose. I dont want them to be included while create
or update
class PetSerializer(serializers.ModelSerializer):
owner_email = serializers.CharField(source='owner.email')
breed_name = serializers.CharField(source='breed.str')
class Meta:
model = Pet
fields = "__all__"
read_only_fields = ["breed_name","owner_email"]
This is not working. I see the owner_email
and breed_name
in the HTMLform (the DRF api page)
Where as
class PetSerializer(serializers.ModelSerializer):
owner_email = serializers.CharField(source='owner.email',read_only=True)
breed_name = serializers.CharField(source='breed.str',read_only=True)
class Meta:
model = Pet
fields = "__all__"
This is working. I dont see them in the HTMLform
Also i observed, if i use a model field directly in read_only_fields then it works.
class PetSerializer(serializers.ModelSerializer):
class Meta:
model = Pet
fields = "__all__"
read_only_fields = ["name"]
This will make all name
not shown in update or create
Why read_only_fields
is not working properly
This is very interesting. I looked into the code and found the root cause, specifically this lines in the implementation for ModelSerializer:
for field_name in field_names:
# If the field is explicitly declared on the class then use that.
if field_name in declared_fields:
fields[field_name] = declared_fields[field_name]
continue
....
Here was my script for the investigation
from django.db import models
from rest_framework import serializers
class MyModel(models.Model):
xero_contact_id = models.UUIDField(unique=True)
name = models.CharField(max_length=255, default="Some name")
class Meta:
db_table = "my_model"
class MySerializer(serializers.ModelSerializer):
owner_email = serializers.CharField()
breed_name = serializers.CharField(max_length=255)
class Meta:
model = MyModel
fields = '__all__'
read_only_fields = ["breed_name", "owner_email", "xero_contact_id"]
serializer = MySerializer()
print(repr(serializer))
I added some prints and here is what I saw:
>>> print(repr(serializer))
field_names ['id', 'owner_email', 'breed_name', 'xero_contact_id', 'name']
declared_fields OrderedDict([('owner_email', CharField()), ('breed_name', CharField(max_length=255))])
extra_kwargs {'breed_name': {'read_only': True}, 'owner_email': {'read_only': True}, 'xero_contact_id': {'read_only': True}}
MySerializer():
id = IntegerField(label='ID', read_only=True)
owner_email = CharField()
breed_name = CharField(max_length=255)
xero_contact_id = UUIDField(read_only=True)
name = CharField(max_length=255, required=False)
As you can see, the read_only
argument is in the extra_kwargs
. The problem is that for all the fields that are only declared in the ModelSerializer
itself (visible from declared_fields
) and not in the model class, they don't read from the extra_kwargs
, they just read what was set in the field itself as visible in the code snippet above fields[field_name] = declared_fields[field_name]
then performs a continue
. Thus, the option for read_only
was ignored.
I fixed it by modifying the implementation of ModelSerializer
to also consider the extra_kwargs
even for non-model fields
for field_name in field_names:
# If the field is explicitly declared on the class then use that.
if field_name in declared_fields:
field_class = type(declared_fields[field_name])
declared_field_args = declared_fields[field_name].__dict__['_args']
declared_field_kwargs = declared_fields[field_name].__dict__['_kwargs']
extra_field_kwargs = extra_kwargs.get(field_name, {})
# Old implementation doesn't take into account the extra_kwargs
# fields[field_name] = declared_fields[field_name]
# New implementation takes into account the extra_kwargs
fields[field_name] = field_class(*declared_field_args, **declared_field_kwargs, **extra_field_kwargs)
continue
....
Now, read_only
was correctly set to the target fields, including non-model fields:
>>> print(repr(serializer))
field_names ['id', 'owner_email', 'breed_name', 'xero_contact_id', 'name']
declared_fields OrderedDict([('owner_email', CharField()), ('breed_name', CharField(max_length=255))])
extra_kwargs {'breed_name': {'read_only': True}, 'owner_email': {'read_only': True}, 'xero_contact_id': {'read_only': True}}
MySerializer():
id = IntegerField(label='ID', read_only=True)
owner_email = CharField(read_only=True)
breed_name = CharField(max_length=255, read_only=True)
xero_contact_id = UUIDField(read_only=True)
name = CharField(max_length=255, required=False)
This doesn't seem to be in the DRF docs. Sounds like a feature we can request to DRF :) So the solution for the meantime is as what @JPG pointed out, use read_only=True
explicitly in the extra non-model fields.