Django JSONStore

Blog · Oracle

ORA-40478: a JSON value too long for a Django query on Oracle

A JSONField on Oracle holds a document of any size. A query that reads one string key out of that document does not. The limit for the extracted value is 4000 characters, and the behaviour at each side of the limit is different.

class Doc(models.Model):
    data = models.JSONField(default=dict)

Doc.objects.create(data={"note": "x" * 5000})

Doc.objects.get(pk=1).data["note"]           # 5000 -- correct
Doc.objects.values_list("data__note", flat=True)[0]
# '' -- empty, and no error

The document is intact. A read of the whole column returns all 5000 characters on every attempt. Only a query that extracts the one key fails. Django compiles that key into a key transform, and the transform asks Oracle for a value of at most 4000 characters.

Length of the string Read of the whole column Read of one key filter(key=value)
4000 or fewerfull valuefull valueORA-40478
4001 or morefull valueempty, no error0 rows, no error

Above 4000 characters the database reports no fault, and the query returns the wrong answer. filter() matches no row, and the row is present. values_list() gives an empty string, and the text is present. A startswith on the first three characters also matches nothing. To find the cause, compare a read of the whole column with a read of the one key. A difference between the two proves the limit, not a fault in the data.

Ask Oracle for a CLOB

Oracle returns the full value when the extraction states a wider return type. JSON_VALUE accepts RETURNING CLOB, and Django reaches it through RawSQL.

from django.db.models import TextField
from django.db.models.expressions import RawSQL

Doc.objects.annotate(
    note=RawSQL("JSON_VALUE(data, '$.note' RETURNING CLOB)", [],
                output_field=TextField())
).values_list("note", flat=True)
# 5000 -- the full value

RETURNING VARCHAR2(32767) also returns the full value, up to that length. Both forms are Oracle syntax, so a project that runs on more than one database needs a branch on connection.vendor.

The better answer is a column

A long text value belongs in a column of its own. A document is the correct place for a short scalar that a query reads: a name, a status, a date, an amount. It is the wrong place for a note field, a description, a rendered template, or a base64 payload.

class Doc(models.Model):
    data = models.JSONField(default=dict)   # short scalars
    note = models.TextField(blank=True)     # the long text

The column costs one migration. It removes the limit, it permits an index, and it keeps the same query behaviour on every database.

The other four databases

PostgreSQL, MySQL, MariaDB and SQLite have no limit of this kind. A read of a 32767-character value through a key transform returns the whole string on PostgreSQL. Thus a test suite on any of the four passes, and the fault appears after the deploy to Oracle.

Measured on Oracle Database 23ai Free and PostgreSQL 17, with Django 6.1 and the oracledb driver in thin mode. The lengths 3999, 4000, 4001 and 5000 were each tested against eight query shapes. Oracle stores a Django JSONField as NCLOB, so the document itself has no practical size limit.