Search code examples
djangodecimaldjango-jsonfieldjsonfield

Django djsonfield Decimal stored as string in expressions


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?


Solution

  • 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 :)