Skip to content

User Guides: JSON and ARRAY Types

For databases that support them (most notably PostgreSQL), DuoORM provides expressive, Pythonic helpers for querying complex JSON and ARRAY data types. These helpers, json() and array(), allow you to write readable filters without needing to remember dialect-specific SQL operators.

Tip

While many databases can store JSON as text, the rich operator support described here is primarily available on PostgreSQL. The helpers will raise a TypeError if used on an unsupported database dialect.

JSON Helpers

To work with a JSON or JSONB column, first define it on your model using the standard SQLAlchemy types.

from duo_orm import Mapped, mapped_column, JSON
from ..database import db

class Device(db.Model):
    __tablename__ = "devices"
    id: Mapped[int] = mapped_column(primary_key=True)

    # A JSONB column to store arbitrary metadata
    metadata: Mapped[dict] = mapped_column(JSON, nullable=False)

Querying with json()

To filter on the contents of the metadata column, import the json helper from duo_orm and use it inside a .where() clause. The helper uses standard Python dictionary access ([]) to navigate the JSON structure.

from duo_orm import json

# Find devices where the 'status' key is 'active'
active_devices = await Device.where(
    json(Device.metadata)["status"] == "active"
).all()

# Find devices where a nested flag is true
beta_devices = await Device.where(
    json(Device.metadata)["flags"]["is_beta"].is_true()
).all()

Casting Values

JSON values are stored as strings internally. To perform typed comparisons, you can use casting helpers.

  • .as_integer()
  • .as_float()
  • .as_boolean()
  • .as_text() (Default)
# Find devices where the 'retries' count is greater than 5
high_retry_devices = await Device.where(
    json(Device.metadata)["telemetry"]["retries"].as_integer() > 5
).all()

Available Operators

Method Description Example
json(col)[key] Navigate a path json(Device.metadata)["status"]
== or .equals(val) Equality check (auto-casts for scalars) json(Device.metadata)["status"] == "active"
!= or .not_equals(val) Inequality check json(Device.metadata)["status"] != "archived"
.is_null() / .is_not_null() Check if a path exists and is (or is not) null. json(Device.metadata)["archived_at"].is_null()
.is_true() / .is_false() Boolean checks (requires a boolean value in JSON) json(Device.metadata)["flags"]["is_beta"].is_true()
.contains(fragment) Check if the JSON contains a given sub-document. json(Device.metadata).contains({"flags": {"is_beta": True}})
.has_key(key) Check if a JSON object has a specific top-level key. json(Device.metadata)["flags"].has_key("is_beta")

All these helpers generate standard SQLAlchemy expressions, so they can be combined with &, |, and other model filters.

ARRAY Helpers

Similarly, you can query ARRAY columns using the array() helper.

First, define a model with an array column. Note that you must import the specific array type from the dialect you are using (e.g., PostgreSQL).

from duo_orm import Mapped, mapped_column, String, PG_ARRAY
from ..database import db

class Article(db.Model):
    __tablename__ = "articles"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str]

    # An array of string tags
    tags: Mapped[list[str]] = mapped_column(PG_ARRAY(String), nullable=False)

Querying with array()

Import the array helper to build expressive membership and overlap queries.

from duo_orm import array

# Find all articles tagged with 'python'
python_articles = await Article.where(
    array(Article.tags).includes("python")
).all()

# Find all articles tagged with BOTH 'python' and 'async'
py_async_articles = await Article.where(
    array(Article.tags).includes_all(["python", "async"])
).all()

# Find all articles tagged with EITHER 'guide' OR 'tutorial'
guide_articles = await Article.where(
    array(Article.tags).includes_any(["guide", "tutorial"])
).all()

Available Operators

Method Description Example
.includes(value) True if the array contains the single value. array(Article.tags).includes("orm")
.includes_all(vals) True if the array contains all provided values. array(Article.tags).includes_all(["python", "orm"])
.includes_any(vals) True if the array contains any provided value. array(Article.tags).includes_any(["go", "rust"])
.length() Returns the number of elements in the array. array(Article.tags).length() > 3
.equals(vals) Exact equality match of the entire array. array(Article.tags).equals(["news"])
.not_equals(vals) Inequality match of the entire array. array(Article.tags).not_equals(["obsolete"])

These helpers provide a clean, readable syntax for common array operations, making it easier to leverage the full power of your database.