JSON/JSONB Filtering
Query nested fields within JSON/JSONB columns using dot notation. PostgreSQL-specific.
| Format | Example | SQL path |
|---|---|---|
| Simple field | metadata.name | {name} |
| Nested field | metadata.user.email | {user,email} |
| Array index | metadata.tags[0] | {tags,0} |
| Nested with array | metadata.items[2].name | {items,2,name} |
| Kebab-case | metadata.user-id | {user-id} |
NOTE
A key is recognized as a JSON path if it contains a . or [. The column named by the first path segment must be a json/jsonb column, or the query throws.
Basic Usage
// Column: metadata jsonb
// Data: { "user": { "id": 123, "role": "admin" } }
{ where: { 'metadata.user.role': 'admin' } }
// SQL: "metadata" #>> '{user,role}' = 'admin'All standard operators work with a JSON path key:
{ where: { 'metadata.score': { gt: 80 } } }
{ where: { 'metadata.level': { ilike: '%high%' } } }
{ where: { 'metadata.status': { in: ['pending', 'review'] } } }
{ where: { 'metadata.code': { regexp: '^[A-Z]+$' } } }NOTE
If the path does not exist in a row's JSON, #>> returns NULL - the row is safely excluded, never an error.
Numeric Casting
A JSON #>> extraction is text, so a numeric comparison needs a cast or Postgres raises operator does not exist. IGNIS wraps the extraction in a safe CASE expression, decided per operator:
{ where: { 'metadata.score': { gt: 50 } } }
// SQL: CASE WHEN ("metadata" #>> '{score}') ~ '^-?[0-9]+(\.[0-9]+)?$'
// THEN ("metadata" #>> '{score}')::numeric ELSE NULL END > 50| Operators | Casts to numeric when... |
|---|---|
gt, gte, lt, lte | the operand is a number |
between, notBetween | both bounds are numbers |
eq, ne, neq | the operand is a number |
in, inq, nin | every array element is a number |
like, ilike, nlike, nilike, regexp, iregexp | never - always text |
| direct value (no operator object) | the value is typeof number |
NOTE
A numeric-looking string still passes the cast ("85" -> 85); a non-numeric string or null falls through to NULL and never matches.
The cast applies per operator, not once for the whole object - a mixed object casts only the operators that need it:
{ where: { 'metadata.score': { gte: 1, like: '%a%' } } }
// gte casts to numeric; like stays text - both read the same #>> extractionnot recurses into whatever it wraps, so a numeric operator nested under not still gets cast:
{ where: { 'metadata.score': { not: { gt: 50 } } } }
// SQL: NOT (CASE WHEN ("metadata" #>> '{score}') ~ '^-?[0-9]+(\.[0-9]+)?$'
// THEN ("metadata" #>> '{score}')::numeric ELSE NULL END > 50)Ordering
{ order: ['metadata.priority DESC'] }
// SQL: ORDER BY "metadata" #> '{priority}' DESCNOTE
Ordering uses #> (returns JSONB, preserves native type ordering); where uses #>> (returns text) instead.
Path Validation
Every path component must match /^[a-zA-Z_][a-zA-Z0-9_-]*$|^\d+$/ - a letter/underscore start followed by letters, digits, underscore or hyphen, or a bare digit run for an array index.
'metadata.fieldName' // valid
'data.meta-data' // valid - kebab-case allowed
'data.123invalid' // invalid - starts with a digit outside array-index context
'metadata.field;DROP TABLE' // invalid - throwsNOTE
A path on a non-JSON column also throws: Column 'name' is not a JSON column.
See also
- Filter System Overview - the
filtershape and the fullwhereoperator table - Fields, Order & Pagination - JSON path ordering (
#>, sorted by native JSONB type) - Pattern Matching -
like/ilike/regexpalso work on a JSON path, with no numeric casting - Nested JSON Updates - writing to JSON paths
- Quick Reference - every operator, one line each
Files:
packages/connectors/src/relational/core/repositories/dialect/filter.ts-FilterBuilder,buildJsonWhereCondition/buildJsonOperatorConditions/buildJsonOrderBypackages/connectors/src/relational/core/repositories/dialect/internal/json-utils.ts-isJsonPath,parseJsonPath, path validation regexpackages/filter/src/common/operators.ts-QueryOperatorsconstants