Django djsonfield Decimal stored as string in expressions

1k Views Asked by At

I have a JSONField with some financial data. I cannot store it as a float, because I need precision, so I store it as a string.

My model looks like this

class Finance(models.Model)
    bill_data = JSONField(
        verbose_name=_("Bill data"),
        default=dict,
        blank=True,
    )

then I save

bill_data = dict(paid=str(some_decimal_amount))

Finance.objects.create(bill_data=bill_data)

I try to use Cast to convert it back to Decimal, because I need to use expressions,

Finance.objects.all().annotate(paid=Cast('bill_data__paid', DecimalField()))

I get an error

    return self.cursor.execute(sql, params)
django.db.utils.DataError: invalid input syntax for integer: "none"
LINE 1: ...der"."bill_data", ("myapp_finance"."bill_data")::numeric(No...

Does anyone know how to use str from JSONField in expressions or how to handle Decimal in JSONField correctly?

1

There are 1 best solutions below

2
Vlad On BEST ANSWER

So today I struggled with exactly this. With help, we managed to get it working.

from django.db.models.functions import Cast
from django.db.models.fields.json import KeyTextTransform
from django.db.models import JSONField, DecimalField

from django.db import models


class Finance(models.Model):
    bill_data = JSONField(
        verbose_name="Bill data",
        default=dict,
        blank=True,
    )

OK so first I'm assuming that bill_data__paid is not Null anywhere. If it is, you should first do a:

Finance.objects.filter(bill_data__paid__isnull=False)

to make sure you are not casting any Null fields.

Ok, then let's try what you did:

Finance.objects.annotate(
    paid=Cast("bill_data__paid", output_field=DecimalField()),
)

But we get an error, something along the lines of:

invalid input syntax for type integer: "none" LINE 1: # ...("myapp_finance"."bill_data" -> 'paid') AS numeric(No......

Ok that's not great. What can we do now then? Well, perhaps we need to specify the number of decimal places and max digits for the Decimal field and you are right that we do.

Finance.objects.annotate(
    paid=Cast("bill_data__paid", output_field=DecimalField(max_digits=6, decimal_places=2)),
)

But does that work? Sadly, no. We are now getting the error

cannot cast jsonb string to type numeric

Hmmm okay. not great but at least it's a different error. The issue is that we have a jsonb string. Let's make it a text text string (for lack of better description)

Finance.objects.annotate(
    paid=Cast(KeyTextTransform("paid", "bill_data"), output_field=DecimalField(max_digits=6, decimal_places=2))
)

and now, it will work.

So we have cast our jsonb string to text, we have then cast it to decimal (and you have to specify the number of decimal places and max digits).

The end :)