sqlalchemy filter by json field

Question:

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.

Asked By: Danila Ganchar

||

Answers:

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.

Answered By: Ilja Everilä

if you only use filter(json_obj["key"] ... ) it will convert to sql like
model_name.json_obj -> 'key', which is still a json object,

if you use filter(json_obj["key"].astext ...), the sql will be
model_name.json_obj ->> 'key', the result is a string object.

Answered By: eastonsuo

You can also use raw sql in filter

from sqlalchemy import text

db.session.query(Example).filter(text("CAST(json_field->>'id' AS INTEGER) = 1")

I guess you can use this expression:

db.session.query(Example).filter(
    Example.json_field.op("->>")("id").cast(Integer) == 1
)
Answered By: Serhii