Nested (non model) Serializer in a ModelSerializer

Viewed 148

I have a fairly niche use case that I'm trying to use DRF serializers for. It will be hard to explain precisely what I'm doing, or why I'm doing it, but I need it to work like this.

I have a ModelSerializer, that has nested in it, a regular Serializer, and another ModelSerializer. Something like this:

class ASerializer(Serializer):
     ...some fields

class BSerializer(ModelSerializer):
     class Meta:
         model = Foo
         fields = '__all__'

class CSerializer(ModelSerializer):
     a = ASerializer()
     b = BSerializer(source='*')
     class Meta:
         model = Foo
         fields = ('a', ' b')

I want to use it like this:

s = CSerializer(foo_instance, data={'a': ...some data})
s.is_valid()  # if necessary
out_data = s.data

Expecting:

out_data = {'a': ...some manipulated data, 'b': ...the serialized foo instance}

So I want to have a serializer, that I pass an instance of Foo to, and some data. The instance of foo is simply to be passed to the nested ModelSerializer (BSerializer) to serialize back out (in a very specific way), and the data I pass is just for the nested Serializer (ASerializer) for that to do some logic (possibly validation) on, and send back out. I'm not using this to modify the model instance in any way, at no point will I call save/create/whatever. It's basically just a data transformation leveraging DRF serializers.

The issue I have is that after initialization, and validation (which does pass!) I get the error:

Got AttributeError when attempting to get a value for field `a` on serializer `CSerializer` ... Original exception text was: 'Foo' object has no attribute 'a'

I also appreciate that the following works without error:

x = ASerializer(data=...some data)
x.is_valid()
y = BSerializer(foo_instance)
out_data = {'a': x.data, 'b': y.data}

But it seems like my desired interface, combining these steps by a single serializer, should be possible - but I can't figure it out.

3 Answers

You are mixing two different arguments, the instance and the data arguments. The instance argument is used to pass in the object that needs to be serialized into native datatypes, whereas the data argument takes in native datatypes that need to be deserialized.

What you can do to get your desired interface is to add the relevant data onto your instance before passing it to the serializer:

# Add your data as an attribute to your instance
foo_instance.a = some_data
s = CSerializer(foo_instance)
s.is_valid()
out_data = s.data

Considering that you have two different values that you want your serializer to take in perhaps a better option would be to forego the source='*':

# Why does this even need to be a ModelSerializer? Why not Serializer instead?
class CSerializer(ModelSerializer):
     a = ASerializer()
     b = BSerializer()
     class Meta:
         model = Foo
         fields = ('a', ' b')

And then use it like below by passing a dictionary to it:

s = CSerializer({'a': some_data, 'b': foo_instance})
s.is_valid()
out_data = s.data

This seem to work:

foo_instance.a = ...some data
s = CSerializer(foo_instance)
out_data = s.data

I'm not certain if adding .a property to foo_instance will give any side effect.

I was exposed on same kind of problem on last project.

The way I found is:

# getting object from database :
instance         = MyModel.object.filter(pk = pk)
# changing it value
instance.value   = "my new value" # From request or wathever
# then run into serializer to convert into JSON data
model_to_json    = MyChildSerializer(instance)
# then run again into serializer to validate new data
serializer       = MyChildSerializer(model_to_json.data, context = {"account" : request.user})
if serializer.is_valid() :
    ...

When serializer run, depending of the source type some function will run and some other not. For exemple the function to_internal_value value will be call only during unserialize process.

Parent serializer

class MySerializer(
    serializers.ModelSerializer,
    ) :

    '''
    This serializer should only be called by Serializer
    '''
    class UrlSerializer(serializers.ModelSerializer) :

        class Meta :
            model   = links_models.Url
            fields  = ['slug', 'code']

    duration    = serializers.SerializerMethodField()

    source      = UrlSerializer(read_only = False)

    target      = UrlSerializer(read_only = False)

    '''
    This function is only called during UNSERIALIZE process
    '''
    def to_internal_value(self, data) :
        _data       = OrderedDict()
        duration    = OrderedDict({'duration'   : data['duration']})

        _data.update(duration)
        _data.update(super().to_internal_value(data))
        return _data

    '''
    This function is called during SERIALIZE and UNSERIALIZE process
    '''
    def get_duration(self, object) :
        if isinstance(object, OrderedDict) : # is it unserialize or serialize process?
            return object.pop('duration')
        else :
            MONTH   = 60 * 60 * 24 * 7 * 4
            WEEK    = 60 * 60 * 24 * 7
            week    = int((int(object.duration) % MONTH))
            month   = int(object.duration) - int(week)

            week  = int(week / WEEK if week > 0 else 0)
            month = int(month / MONTH if month > 0 else 0)
            return {"month" : month, "week"  : week}

    class Meta :
        model = MyModel
        field = ['duration', 'source', 'target']

Child Serializer

class MyChildSerializer(MySerializer) :

    '''
    By default read_only is True.
    '''
    state       = serializers.SerializerMethodField()

    '''
    This function is only called during UNSERIALIZE process
    '''
    def to_internal_value(self, data) :
        _data       = OrderedDict()
        state       = OrderedDict({'state'      : data['state']})

        _data.update(state)
        _data.update(super().to_internal_value(data))
        return _data

    '''
    This function is called during SERIALIZE and UNSERIALIZE process
    '''
    def get_state(self, object) :
        if isinstance(object, OrderedDict) :  # is it unserialize or serialize process?
            return object.pop('state')
        else :
            state = object.state

            state = LeaseStateSerializer(
                    state,
                    read_only  = True,
                    )
        return state.data

    def validate_state(self, value) :
        if not value == =MyModel.WAIT :
            raise serializers.ValidationError(
                {'state' : _("An error occure; action not permited")}
                )
        return value

    '''
    This function is only called during UNSERIALIZE process
    '''
    def validate(self, data) :
        if not self.context.get("account") :
            raise Exception("Account is require in context when submiting " \
            "validation on %s" % (MyChildSerializer.__name__)
            )
        validate = super().validate(data)
        self.validate_state(validate.get('state'))
        return (validate)

    class Meta :
        model = MyModel
        field = ['duration', 'source', 'target', 'state']

Related