pythonsqlalchemyflask-sqlalchemy

sqlalchemy filter by json field


I have model with json column. Example of model and data:

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgres://...'

db = SQLAlchemy()
db.init_app(app)
app.app_context().push()

class Example(db.Model):
    id = db.Column(db.Integer(), nullable=False, primary_key=True, )
    json_field = db.Column(db.JSON())

db.create_all()
db.session.add(Example(json_field={'id': None}))
db.session.add(Example(json_field={'id': 1}))
db.session.add(Example(json_field={'id': 50}))
db.session.add(Example(json_field={}))
db.session.commit()

Now I try to find records where id == 1:

query = db.session.query(Example).filter(Example.json_field['id'] == 1)
print(query.all())

And I getting the next error:

sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) operator does not exist: json = integer LINE 3: WHERE (example.json_field -> 'id') = 1

The reason. Look at generated query:

SELECT example.id AS example_id, example.json_field AS example_json_field 
FROM example 
WHERE (example.json_field -> %(json_field_1)s) = %(param_1)s

But in my case correct query should be like this:

SELECT * FROM example WHERE CAST(json_field->>'id' AS INTEGER) = 1;

How can I do this?

I have tried use cast, but unsuccessfully:

print(
    db.session.query(Example).filter(
        cast(Example.json_field['id'], Integer) == 1
    ).all()
)

The error:

sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) cannot cast type json to integer LINE 3: WHERE CAST((example.json_field -> 'id') AS INTEGER) = 1

As you can see where clause still wrong. Also I need to use range (>, <= etc.) conditions. Thanks for help.


Solution

  • Flask-SQLAlchemy's SQLAlchemy object – commonly named dbgives access to functions etc. from sqlalchemy and sqlalchemy.orm, and so db.JSON is the generic JSON type that does not provide the Postgresql specific operators. You should instead use sqlalchemy.dialects.postgresql.JSON:

    from sqlalchemy.dialects.postgresql import JSON
    
    class Example(db.Model):
        id = db.Column(db.Integer(), nullable=False, primary_key=True, )
        json_field = db.Column(JSON)
    

    With the proper type in place you must explicitly convert the JSON to text first and then cast to an integer:

    db.session.query(Example).\
        filter(Example.json_field['id'].astext.cast(Integer) == 1)
    

    This produces the desired predicate

    CAST(json_field->>'id' AS INTEGER) = 1
    

    The same applies to all types that cannot be directly cast from json. SQLAlchemy used to offer a shortcut for the combination of astext and cast(), but it has been removed in version 1.1 and above:

    Changed in version 1.1: The ColumnElement.cast() operator on JSON objects now requires that the JSON.Comparator.astext modifier be called explicitly, if the cast works only from a textual string.