60

Let's say I have a Postgres database (9.3) and there is a table called Resources. In the Resources table I have the fields id which is an int and data which is a JSON type.

Let's say I have the following records in said table.

  • 1, {'firstname':'Dave', 'lastname':'Gallant'}
  • 2, {'firstname':'John', 'lastname':'Doe'}

What I want to do is write a query that would return all the records in which the data column has a json element with the lastname equal to "Doe"

I tried to write something like this:

records = db_session.query(Resource).filter(Resources.data->>'lastname' == "Doe").all()

Pycharm however is giving me a compile error on the "->>"

Does anyone know how I would write the filter clause to do what I need?

4
  • Try using astext like: ...filter(Resources.c.data["lastname"].astext == "Doe").all() and see if it works Commented Apr 30, 2015 at 18:23
  • That seems to work, though I had to remove the ".c" and just go "Resources.data" directly. Thanks! Commented Apr 30, 2015 at 18:30
  • Ah... yes, I usually go with .c for column name... glad you have it solved. I will post above as an answer then :-) Commented Apr 30, 2015 at 18:31
  • You must use the .c if you are not use declarative base I think (table = sqlalchemy.Table(...)) Commented May 11, 2020 at 1:41

6 Answers 6

92

Try using astext

records = db_session.query(Resource).filter(
              Resources.data["lastname"].astext == "Doe"
          ).all()

Please note that the column MUST have a type of a JSONB. The regular JSON column will not work.

Sign up to request clarification or add additional context in comments.

1 Comment

Great answer. In addition, the link you gave also provides some other information on how to query nested json (eg query.filter(Resources.data[("lastname", "whatever-other-nested-json-under-lastname")].astext == "Whatever Value").all() ), and how to change data types.
16

Also you could explicitly cast string to JSON (see Postgres JSON type doc).

from sqlalchemy.dialects.postgres import JSON
from sqlalchemy.sql.expression import cast
db_session.query(Resource).filter(
    Resources.data["lastname"] == cast("Doe", JSON)
).all()

3 Comments

You may need to import from dialects.postgresql not just postgres
For larger tables, dont forget to create a partial index. Otherwise JSON queries become expensive. CREATE INDEX idx_lastname_partial ON resourses (((data ->> 'lastname')::VARCHAR(24))) WHERE (data ->> 'lastname') IS NOT NULL;
You should use JSONB instead of JSON. Then you can articulate more sophisticated where clauses such as Resources.data["size"] > cast(0.4, JSONB)
5

I have some GeoJSON in a JSON (not JSONB) type column and none of the existing solutions worked, but as it turns out, in version 1.3.11 some new data casters were added, so now you can:

records = db_session.query(Resource).filter(Resources.data["lastname"].as_string() == "Doe").all()

Reference: https://docs.sqlalchemy.org/en/14/core/type_basics.html#sqlalchemy.types.JSON

Casting JSON Elements to Other Types

Index operations, i.e. those invoked by calling upon the expression using the Python bracket operator as in some_column['some key'], return an expression object whose type defaults to JSON by default, so that further JSON-oriented instructions may be called upon the result type. However, it is likely more common that an index operation is expected to return a specific scalar element, such as a string or integer. In order to provide access to these elements in a backend-agnostic way, a series of data casters are provided:

Comparator.as_string() - return the element as a string

Comparator.as_boolean() - return the element as a boolean

Comparator.as_float() - return the element as a float

Comparator.as_integer() - return the element as an integer

These data casters are implemented by supporting dialects in order to assure that comparisons to the above types will work as expected, such as:

# integer comparison
data_table.c.data["some_integer_key"].as_integer() == 5

# boolean comparison
data_table.c.data["some_boolean"].as_boolean() == True

2 Comments

What about if I have a Big | Long Integer and doen't fill in a Integer type?
This commentary is out of bounds from the question scope. but I found an error while compare using the as_integer function, so I pick this expression cast(Table.column, JSONB)['json_property'].as_numeric(13, 0) == number, the as_numeric from SQLAlchemy Docs helps!
4

If you are using JSON type (not JSONB) the following worked for me:

Note the '"object"'

    query = db.session.query(ProductSchema).filter(
        cast(ProductSchema.ProductJSON["type"], db.String) != '"object"'
    )

Comments

3

According sqlalchemy.types.JSON, you can do it like this

from sqlalchemy import JSON
from sqlalchemy import cast
records = db_session.query(Resource).filter(Resources.data["lastname"] == cast("Doe", JSON)).all()

Comments

2

According to this, pre version 1.3.11, the most robust way should be like this, as it works for multiple database types, e.g. SQLite, MySQL, Postgres:

from sqlalchemy import cast, JSON, type_coerce, String

db_session.query(Resource).filter(
    cast(Resources.data["lastname"], String) == type_coerce("Doe", JSON)
).all()

From version 1.3.11 onward, type-specific casters is the new and neater way to handle this:

db_session.query(Resource).filter(
    Resources.data["lastname"].as_string() == "Doe"
).all()

1 Comment

Thanks @Xuan this is the only answer that worked for me

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.