Receiving an array of objects via form

I have a Django model like this:

class SomeModel(BaseModel):
    description = models.CharField(max_length=100)
    case = models.ForeignKey(Case, on_delete=models.CASCADE)
    is_hidden = models.BooleanField(default=False)
    data = models.JSONField(default=list)

I want to store JSON data in the data field on the model like this:

[{"year": 1, "amount": 10000}, {"year": 2,  "amount": 234}, {"year": 3, "amount": 230000, "description": "to principal"}]

I’ve got it working on the client side by inserting inputs with name values like data[][year] inside the form:

<input class="form-text-input" type="number" name="data[][year]" required>

The form is sending to the server and my request.POST is getting it:

 <QueryDict: {'csrfmiddlewaretoken': ['xxx'], 'description': ['affa2324'], 'data[][year]': ['1'], 'data[][amount]': ['2342'], 'data[][description]': ['foo'], 'data[][year]': ['2'], 'data[][amount]': ['2355'], 'data[][description]': ['bar']}>

Where I’m having trouble is getting Django to understand that this form data is an array of dictionary values. So far I’ve been handling it by changing the name values to data_year[] and handling it like this:

    year = request.POST.getlist("data_year[]")
    amount = request.POST.getlist("data_amount[]")
    description = request.POST.getlist("data_description[]")
    data = json.dumps(
        [{"year": y, "amount": a, "description": d} for y, a, d in zip(year, amount, description)]
    )

But it doesn’t scale very well for me in that I have to specialize this pattern for every field in my JSON.

Other frameworks I’ve worked with have understood key[][value] to be an array of dictionaries, am I missing some better way to do this with Django?

Yes.

To identify what those better solutions are is going to require more detailed and specific information about the data being posted and how it’s being submitted.

How are you submitting this data?
Is this being done as a normal form submission, or is there AJAX involved?
If this is a forms submission, what do the forms look like?
(If this is an AJAX submission, then my initial reaction would be to suggest that you submit the data as a JSON object, processing it accordingly within your view.)

I’m just appending fields to the form via Alpine.js. The form is being submitted just as a normal form submission. The form looks like this:

<form method="post"
        class=""
        x-data="{ forms: {{ payment.data|default:"[]" }} }">
    {% csrf_token %}
    {{ form }}
    <div class="">
        <template x-for="(form, index) in forms" :key="index">
            <div class=">
                <div>
                    <label class="form-label">
                        Year <span class="text-red-500">*</span>
                    </label>
                    <input class="form-text-input"
                            type="number"
                            name="data[][year]"
                            min="1"
                            max="99"
                            x-model="form.year"
                            required>
                </div>
                <div>
                    <label class="form-label">
                        Kind <span class="text-red-500">*</span>
                    </label>
                    <select class="form-text-input"
                            name="data[][kind]"
                            x-model="form.kind"
                            required>
                        <option value="">Select</option>
                        {% for kind in kind_choices %}<option value="{{ kind.0 }}">{{ kind.1 }}</option>{% endfor %}
                    </select>
                </div>
                <div x-show="form.kind == 'income' || form.kind == 'custom_payment'">
                    <label class="form-label">
                        Amount <span class="text-red-500">*</span>
                    </label>
                    <input class="form-text-input"
                            type="number"
                            name="data[][amount]"
                            x-model="form.amount"
                            :required="form.kind == 'income' || form.kind == 'custom_payment'"
                            step="0.01">
                </div>
                <div x-show="form.kind == 'income' || form.kind == 'custom_payment'">
                    <label class="form-label">
                        Description <span class="text-red-500">*</span>
                    </label>
                    <input class="form-text-input"
                            type="text"
                            name="data[][description]"
                            :required="form.kind == 'income' || form.kind == 'custom_payment'"
                            x-model="form.description">
                </div>
                <div class="">
                    <label class="form-label">Actions</label>
                    <button class="error btn" type="buttton" @click="forms.splice(index, 1)">Remove</button>
                </div>
            </div>
        </template>
        <div class="w-full py-3">
            <button @click="forms.push({year: forms.length+1, kind: '', amount: '', description: ''})"
                    class="btn"
                    type="button"
                    id="add-data-form">Add Year</button>
        </div>
    </div>
    <button class="self-center btn-accent" type="submit">
        {% if editing %}
            Update
        {% else %}
            Create
        {% endif %}
    </button>
</form>

I ended up with this function:

def parse_form_lists(request):
    """Parse nested form data from a POST request into a list of dicts"""
    form_data = {}
    for list_key, list_value in request.POST.lists():
        if "[][" in list_key:
            key, value = list_key.split("[][")
            if not form_data.get(key):
                form_data[key] = []
            value = value.rstrip("]")
            for idx, val in enumerate(list_value):
                if len(form_data[key]) <= idx:
                    form_data[key].append({})
                form_data[key][idx][value] = val
    return form_data

and now I just call it from my view and get the key I’m after and send it to my form:

def myView(request):
    if request.method == "POST":
        form_data = parse_form_lists(request)
        initial |= form_data
        form = MyForm(request.POST, request.FILES, initial=initial)

etc.