Django JSONFIeld's widget for data input in format: forms.Select (key) - forms.TextInput (value)

I have a hard times to implement "user friendly" JSON input within a form.

I have some Item model which contains attributes = JSONFIeld().

class ItemType(models.Model):
    title = models.CharField()

class Item(models.Model):
    title = models.CharField()
    item_type = models.ForeignKey(ItemType)
    attributes = models.JSONField()

To keep particular "schema" within a single ItemType I've added models:

class ItemAttribute(models.Model):
    title = models.CharField(max_length=100, unique=True)

class ItemAttributeSpec(models.Model):   
    item_type = models.ForeignKey(ItemType)  
    attribute = models.ForeignKey(ItemAttribute)  
    required = models.BooleanField(default=False)
    choices = models.JSONField(default=list)

So a goal for implementation: Provide a set of attributes' key/value pairs where key of JSON field will be forms.Select() or just label (doesn't matter much as I can manage implementation of this feature) and value is an input. So every single Item form instance would has all type-related attributes for input. Generally some kind of formset within a single form instance.

Something like this: enter image description here

Using helper models to define the schema but not contain the value, is basically all the hassle of an EAV-setup with almost none of its benefits, IMO.

You'd be doing double bookeeping by storing the keys in the JSON on Item in addition to defining the keys and all their metadata in all those helpers. That makes the helpers largely irrelevant except for schema validation purposes, and there are bound to be easier ways to do that unless you have very particular and special business needs.

Instead, here's a single-form solution that doesn't use any helper models and the schema is defined by a dict at runtime:

class DynamicKVForm(forms.Form):
    def __init__(self, *args, **kwargs):
        kv_pairs = kwargs.pop('json_schema', [])
        super().__init__(*args, **kwargs)

        for field in kv_pairs:
            key = field['key']

            field_kwargs = {
                'label': field.get('label', key.capitalize()),
                'required': field.get('required', False),
                'help_text': field.get('help_text', ''), # <-- You can optionally pass `help_text` in the input to override this on a per-field basis
                'initial': field.get('initial', None),   # <-- Same for this one
            }

            self.fields[key] = forms.CharField(**field_kwargs)

You can expand this functionality by adding a field to the schema where you define widget type, etc., and use that information to fetch widgets dynamically rather than making everything CharField.

Usage:

form = DynamicKVForm(
    json_schema=[
        {"key": "name", "label": "Full name", "required": True},
        {"key": "email", "label": "Email address", "required": True},
    ]
)

If you need to store the schema in db, one easy solution is to store it on Item in a separate JSONField attribute.

Back to Top