Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Serializer for JSONField nested representation

In my application there's a model that stores a configuration in one of its fields. The field is defined as JSONField. I have a strict structure that defines what content of this field should look like, but I'm struggling to find a way how to serialize it to validate data in API requests.

The solution that works for now, but does not validate what's inside config_field, is blindly accept anything that complies with being a json object:

A simplified version of my model:

class MyModel(models.Model):
    config_field = JSONField(...)
    ...

For the sake of this question here's a simplified version of the data structure stored in config_field:

{"some_config_int": 42, "some_config_vars": [{"id": 1}, {"id": 2}]}

And here's a simplified version of my serializer:

class MyModelSerializer(serializers.ModelSerializer):
        config_field = serializers.JSONField(required=False)
        class Meta:
            model = MyModel
            fields = ('config_field', ...)

What I'd like to achieve though is to have a serializer for the nested representation (reference to DRF documentation) of what's inside config_field. What I've tried so far (but does not work):

class ConfigVarsSerializer(serializers.Serializer):
        id = serializers.IntegerField(required=True)

class ConfigFieldsSerializer(serializers.Serializer):
    some_config_int = serializers.IntegerField(required=True)
    some_config_vars = serializers.ListField(child=ConfigVarsSerializer,required=True)

class MyModelSerializer(serializers.ModelSerializer):
            config_field = ConfigFieldsSerializer(required=False)
            class Meta:
                model = MyModel
                fields = ('config_field', ...)

This way it will be optional to POST/PUT an object with a configuration, but if config_field is in the body of the request, the entire nested object should be provided.

like image 918
Stan Redoute Avatar asked Sep 15 '25 04:09

Stan Redoute


1 Answers

After trying several possible solutions I want to point out 2 simplest and most importantly those that do not require overriding create method for neither MyModelSerializer nor inner serializers:

  1. Override field validation method for config_field in MyModelSerializer
  2. Override validate method for entire object being serialized by MyModelSerializer

Serializers representing inner content of config_field would be the same for both solutions:

class ConfigVarsSerializer(serializers.Serializer):
    id = serializers.IntegerField(required=True)

class ConfigFieldsSerializer(serializers.Serializer):
    some_config_int = serializers.IntegerField(required=True)
    some_config_vars = serializers.ConfigVarsSerializer(required=True, many=True)

Note that some_config_vars stores list of objects, that's why many=True.


Solution 1

Override field validation method for config_field in MyModelSerializer. In case of given example the final code of serializer would be:

class MyModelSerializer(serializers.ModelSerializer):
        config_field = JSONField(required=False)
        class Meta:
            model = MyModel
            fields = ('config_field', ...)

        def validate_config_field(self, value):
            serializer = ConfigFieldsSerializer(data=value)
            serializer.is_valid(raise_exception=True)
            return value

This approach first validates config_field using default JSONFieldSerializer and raises exception if content isn't a valid JSON object.

If JSONFieldSerializer raises no exception validate_custom_fields is called and it passes content of field into ConfigFieldsSerializer and validates all content for itself and all nested serializers.


Solution 2

Override validate method for entire object being serialized by MyModelSerializer. In case of given example the final code of serializer would be:

class MyModelSerializer(serializers.ModelSerializer):
        config_field = JSONField(required=False)
        class Meta:
            model = MyModel
            fields = ('config_field', ...)

        def validate(self, attrs):
            config_field = attrs.get('config_field')
            if config_field:
                serializer = ConfigFieldsSerializer(data=config_field)
                serializer.is_valid(raise_exception=True)
            return attrs

This approach requires a little bit more code, but allows to combine validation of config_field with other related fields.

like image 133
Stan Redoute Avatar answered Sep 17 '25 18:09

Stan Redoute