Search code examples
djangodjango-rest-frameworkdjango-serializer

Django DRF: read_only_fields not working properly


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


Solution

  • 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.