Skip to content

ValueError in EncryptedType.process_result_value with JSON Column #789

@tommymh

Description

@tommymh

Issue

EncryptedType with a JSON column raises ValueError: dictionary update sequence element #0 has length 1; 2 is required when reading data, due to incorrect handling of decrypted JSON strings in process_result_value. The method attempts to pass a decrypted JSON string (e.g., '{"key": "value"}') to dict(), expecting a sequence of key-value pairs, instead of parsing it with json.loads.

Steps to Reproduce

  1. Save the attached script as bug.py.
  2. Install dependencies: pip install sqlalchemy==2.0.35 sqlalchemy-utils==0.42.3 cryptography==42.0.8.
  3. Run: python bug.py.
  4. Observe ValueError when accessing the id attribute after committing.

Script

import sqlalchemy as sa
from sqlalchemy.orm import declarative_base, sessionmaker
from sqlalchemy_utils import EncryptedType
import os
import json

SECRET_KEY = 'a' * 32
engine = sa.create_engine('sqlite:///:memory:')
Session = sessionmaker(bind=engine)
Base = declarative_base()

class BuggyModel(Base):
    __tablename__ = 'buggy_table'
    id = sa.Column(sa.Integer, primary_key=True)
    encrypted_data = sa.Column(EncryptedType(sa.JSON, SECRET_KEY))

Base.metadata.create_all(engine)

def demonstrate_bug():
    session = Session()
    print("--- Step 1: Writing to the Database ---")
    json_payload = {'message': 'This is a test', 'value': 123}
    instance = BuggyModel(encrypted_data=json_payload)
    session.add(instance)
    session.commit()
    print(f"✅ Successfully committed the object (ID: {instance.id}).")
    print("The data is now encrypted in the database.")
    print("-" * 40)
    print("\n--- Step 2: Reading from the Database ---")
    print("Now, attempting to refresh the object from the database.")
    print("This will trigger EncryptedType's 'process_result_value' method...")
    try:
        session.refresh(instance)
        print("\n❌ BUG NOT REPRODUCED. The library may have been updated.")
        print(f"   Decrypted data: {instance.encrypted_data}")
    except ValueError as e:
        print(f"\n✅ BUG REPRODUCED SUCCESSFULLY!")
        print(f"   Error Type: {type(e)}")
        print(f"   Error Message: {e}")
        error_string = "dictionary update sequence element #0 has length 1; 2 is required"
        if error_string in str(e):
            print("\n   Proof: The error message matches the one from our test suite.")
            print("   This confirms the library's fallback logic is attempting to run `dict('{\"key\": ...}')`.")
        else:
            print("\n   NOTE: A ValueError occurred, but it was not the one we expected.")
    finally:
        session.close()

if __name__ == "__main__":
    demonstrate_bug()

Output

/home/tommy/src/sqlalchemy-utils-bug/bug.py:26: DeprecationWarning: The 'EncryptedType' class will change implementation from 'LargeBinary' to 'String' in a future version. Use 'StringEncryptedType' to use the 'String' implementation.
  encrypted_data = sa.Column(EncryptedType(sa.JSON, SECRET_KEY))
--- Step 1: Writing to the Database ---
Traceback (most recent call last):
  File "/home/tommy/src/sqlalchemy-utils-bug/.venv/lib/python3.10/site-packages/sqlalchemy_utils/types/encrypted/encrypted_type.py", line 427, in process_result_value
    return self.underlying_type.process_result_value(
AttributeError: 'JSON' object has no attribute 'process_result_value'
[Additional stack trace omitted for brevity]
ValueError: dictionary update sequence element #0 has length 1; 2 is required

Environment

Python: 3.10
SQLAlchemy: 2.0.35
SQLAlchemy-Utils: 0.42.3
Cryptography: 42.0.8
Database: SQLite

Expected Behavior

EncryptedType should parse decrypted JSON strings using json.loads to return a Python dictionary (e.g., {"key": "value"}).
Non-dictionary JSON (e.g., ["invalid"], "invalid") should be returned as-is or handled gracefully.

Actual Behavior

ValueError: dictionary update sequence element #0 has length 1; 2 is required during read operations, as process_result_value attempts dict('{"key": "value"}').
DeprecationWarning suggests StringEncryptedType, but it’s unavailable in 0.42.3.
Previous attempts raised TypeError: LargeBinary.init() got an unexpected keyword argument 'secret_key' and TypeError: argument 'data': Cannot convert "<class 'NoneType'>" instance to a buffer.

Additional Notes

Encountered in a FastAPI app with PostgreSQL JSON column (sqlalchemy.dialects.postgresql.JSON). The bug affects read operations after writing valid JSON data, breaking CRUD workflows.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions