Search code examples
djangodjango-jsonfield

Unable to enter json in django admin


In my model I have the following field -

data_fields = ArrayField(models.JSONField(null=True, blank=True), blank=True, null=True)

In order to test this out, I entered the following in the data_field in the django admin -

[{"key": "name", "label": "Name"}, {"key": "amount", "label": "Amount"}]

But I get an error saying

Item 1 in the array did not validate: Enter a valid JSON.
Item 2 in the array did not validate: Enter a valid JSON.
Item 3 in the array did not validate: Enter a valid JSON.
Item 4 in the array did not validate: Enter a valid JSON.

I only get this error if there are multiple key-value pairs in the JSON. If there is only one JSON element with one key-value pair, it works. If there are multiple elements in the array, or there are multiple key-value pairs in the JSON field, I get the error above.

Why is this happening, and how do I fix it?


Solution

  • This is one of the many reasons not to use an ArrayField or JSONField in the first place [django-antipatterns]. ArrayFields are, as far as I know, not very well supported in databases nor in Django, and that results in all sorts of querying problems, and problems with forms. Here for example it will split on a comma, so the first item looks like {"key": "name", that is indeed not valid JSON, the proof of this behavior is that it sees four elements, not two. It thus does not know when to split, and as a result passes incorrect items to the JSON deserializer.

    That being said, why not just use a JSONField [Django-doc], after all, a list is a JSON blob, so using:

    data_fields = models.JSONField(null=True, blank=True, default=list)

    But still, if the structure is fixed, like the JSON blob seems to be, I would advise using an inline:

    class Parent(models.Model):
        pass
    
    
    class ParentDataField(models.Model):
        parent = models.ForeignKey(
            Parent, related_name='data_fields', on_delete=models.CASCADE
        )
        key = models.CharField(max_length=128)
        label = models.CharField(max_length=128)

    in fact, we even can ensure that for the same parent, the key only occurs at most once:

    class Parent(models.Model):
        pass
    
    
    class ParentDataField(models.Model):
        parent = models.ForeignKey(
            Parent, related_name='data_fields', on_delete=models.CASCADE
        )
        key = models.CharField(max_length=128)
        label = models.CharField(max_length=128)
    
        class Meta:
            constraints = [
                models.UniqueConstraint(
                    fields=('parent', 'key'), name='unique_key_per_parent'
                )
            ]

    and then work with an inline:

    from django.contrib import admin
    
    
    class ParentDataFieldInline(admin.TabularInline):
        model = ParentDataField
    
    
    class ParentAdmin(admin.ModelAdmin):
        inlines = [
            ParentDataFieldInline,
        ]