Blog · Oracle
A Django JSON column on Oracle returns a LOB, not a string
A JSONField holds text, so a raw cursor looks like the quick way to read
it. On Oracle that cursor returns a LOB object. The value is not a string, and
json.loads() rejects it.
with connection.cursor() as cursor:
cursor.execute("SELECT data FROM app_job WHERE ROWNUM = 1")
value = cursor.fetchone()[0]
type(value)
# PostgreSQL: <class 'str'>
# Oracle: <class 'oracledb.LOB'>
json.loads(value)
# TypeError: the JSON object must be str, bytes or bytearray, not LOB
Oracle stores a large text value as a Character Large Object. The driver does not transfer the content with the row. It returns a handle, and the content arrives at the first read. The handle behaves like a file, and it holds a cursor position.
Read the handle first
Call read() on the value to get the text. Accept both types, because the same code
then runs on every database.
def as_text(value):
if value is None or isinstance(value, str):
return value
if hasattr(value, "read"): # Oracle LOB
return value.read()
if isinstance(value, (bytes, bytearray, memoryview)):
return bytes(value).decode()
return str(value)
json.loads(as_text(value))
# {'qty': 1}
A LOB reads one time. The handle keeps a position, so a second
read() returns an empty string, and a read() after the cursor closes
raises an error. Assign the result of the first read() to a variable, and use that
variable. A log line that prints the value counts as a read.
The ORM does this already
Django's JSONField holds a converter for each backend. A query through the ORM
returns the decoded object on Oracle and on PostgreSQL, with no extra code.
Job.objects.values_list("data", flat=True)[0]
# {'qty': 1} on every backend
Job.objects.filter(data__qty=1) # also fine
Thus the fault appears only in code that goes around the ORM. Four common places are a raw cursor,
a Model.objects.raw() call, a report command, and a data migration. The data migration
is the most common of the four. A migration often uses SQL to keep away from the model classes.
Measured on Oracle Database 23ai Free with the oracledb driver and Django 6.1.
The same helper is in the
test suite of django-jsonstore,
where the assertions on the stored document have to read the raw column.