Advanced Querying

Advanced JSON querying for the `json(field, path)` function and `_json` filter operator, including path notation, relational queries, GraphQL support, SDK usage, depth limits, and database-specific behavior.

This page covers advanced JSON querying in Directus. For a brief introduction with basic syntax and examples, see the quickstart.

Path Notation

Paths use dot notation for object keys and bracket notation for array indices.

PatternExampleMeaning
keycolorTop-level object key
a.b.csettings.theme.colorNested object key
[n]tags[0]Array element at index n
a[n].bitems[0].nameMixed object/array access

Examples:

json(metadata, settings.theme)

Unsupported Path Expressions

The following path syntaxes are not supported and and will result in an error if used

ExpressionExample
Empty brackets (wildcard)items[]
[*] wildcarditems[*].name
* globitems.*
JSONPath predicatesitems[?(@.price > 10)]
@ current node@.name
$ root$.name

Non-Alphanumeric Characters in Object Keys

The path syntax uses . to separate key segments and does not provide an escape mechanism. As a result, object keys that contain dots, spaces, or other special characters cannot be accessed. For example, the key "first.name" is interpreted as access to the nested key name inside the key first.

The json(field, path) Function

The json(field, path) function retrieves the value at the specified path within a JSON document. It can be used wherever a field reference is accepted, including the fields, sort, and alias query parameters.

Not Supported in Filters The json(field, path) function is not supported in the filter query parameter. For filtering JSON fields, use the _json filter operator.

Syntax

json(field, path)
  • field (required): The name of a JSON column in the collection, or a relational path leading to one.
  • path (required): A dot-and-bracket notation path used to extract a specific value from within the JSON document.
In GraphQL, each json type field exposes a json(path: String!) sub-field within {fieldName}_func which should be used instead. The return type is JSON, which can be a scalar, object, or array.
The SDK supports a type safe json(field, path) expression within its fields array, see SDK Type Safety for more deatils.

Response Format

For REST and the SDK, extracted values are returned as additional fields on each item using auto-generated aliases.

The alias follows the pattern:

{field}_{path}_json

Path segments are normalized by replacing special characters (e.g. [, ], .) with underscores.

Request fieldResponse key
json(metadata, color)metadata_color_json
json(metadata, settings.priority)metadata_settings_priority_json
json(data, items[0].name)data_items_0_name_json
In GraphQL, the extracted value is returned under {fieldName}_func.json. When requesting multiple paths for the same field, use GraphQL field aliases to distinguish them.

Basic Example

import { createDirectus, rest, readItems } from "@directus/sdk";
const directus = createDirectus("https://directus.example.com").with(rest());

const result = await directus.request(
  readItems("articles", {
    fields: ["id", "title", "json(metadata, color)"],
  }),
);

Response:

{
  "data": [
    {
      "id": 1,
      "title": "An Article",
      "metadata_color_json": "blue"
    }
  ]
}

Multiple Paths

Extract multiple values from a single JSON field in one request. In GraphQL, use field aliases on the json sub-field to differentiate each extracted value.

import { createDirectus, rest, readItems } from "@directus/sdk";
const directus = createDirectus("https://directus.example.com").with(rest());

const result = await directus.request(
  readItems("articles", {
    fields: [
      "id",
      "json(metadata, color)",
      "json(metadata, settings.theme)",
      "json(metadata, tags[0])",
    ],
  }),
);

Response:

{
  "data": [
    {
      "id": 1,
      "metadata_color_json": "blue",
      "metadata_settings_theme_json": "dark",
      "metadata_tags_0_json": "featured"
    }
  ]
}

Extracting an Object or Array

When the path points to an object or array rather than a scalar, the full value is returned as parsed JSON.

Non-Scalar Paths in Sort and Filter Sorting or filtering by a path that resolves to an object or array can produce unexpected results. The database compares the serialized form, which depends on dialect-specific JSON ordering and formatting. Use paths that resolve to a scalar value (string, number, boolean) for reliable sorting and filtering.
import { createDirectus, rest, readItems } from "@directus/sdk";
const directus = createDirectus("https://directus.example.com").with(rest());

const result = await directus.request(
  readItems("articles", {
    fields: ["id", "json(metadata, dimensions)", "json(metadata, tags)"],
  }),
);

Response:

{
  "data": [
    {
      "id": 1,
      "metadata_dimensions_json": { "width": 100, "height": 50 },
      "metadata_tags_json": ["featured", "new"]
    }
  ]
}

Relational Queries

json(field, path) can traverse relational fields to extract JSON values from related items. The relational path is included in the first argument, before the JSON field name.

Many-to-One (M2O)

Syntax: json(relation.json_field, path)

The extracted value is returned nested under the relational key in the response, alongside other requested fields from the same relation. Multiple json(field, path) extractions in the same relation are grouped under the same relational key.

import { createDirectus, rest, readItems } from "@directus/sdk";
const directus = createDirectus("https://directus.example.com").with(rest());

const result = await directus.request(
  readItems("articles", {
    fields: ["id", "title", { category_id: ["name", "json(metadata, color)"] }],
  }),
);

Response:

{
  "data": [
    {
      "id": 1,
      "title": "An Article",
      "category_id": {
        "name": "News",
        "metadata_color_json": "blue"
      }
    }
  ]
}

One-to-Many (O2M)

Syntax: json(relation.json_field, path)

For O2M relations, each related item returns its own extracted value. The response contains an array of objects, each with the extracted key.

import { createDirectus, rest, readItem } from "@directus/sdk";
const directus = createDirectus("https://directus.example.com").with(rest());

const result = await directus.request(
  readItem("articles", 1, {
    fields: ["id", { comments: ["json(data, type)"] }],
  }),
);

Response:

{
  "data": {
    "id": 1,
    "comments": [
      { "data_type_json": "comment" },
      { "data_type_json": "review" }
    ]
  }
}

Many-to-Any (M2A)

Syntax: json(relation.item:collection_name.json_field, path)

M2A relations, use the standard Directus collection scope syntax inside the first argument.

import { createDirectus, rest, readItem } from "@directus/sdk";
const directus = createDirectus("https://directus.example.com").with(rest());

const result = await directus.request(
  readItem("shapes", 1, {
    fields: [
      "id",
      {
        children: [
          {
            item: {
              circles: ["json(metadata, color)"],
            },
          },
        ],
      },
    ],
  }),
);

Response:

{
  "data": {
    "id": 1,
    "children": [
      {
        "item": {
          "metadata_color_json": "red"
        }
      }
    ]
  }
}

Depth Limits

json(field, path) enforces two independent depth limits:

  • Relational depth (MAX_RELATIONAL_DEPTH, default 10): Limits how deeply relational selections can go in the field argument. For example, json(category_id.metadata, a.b.c.d.e) has a relational depth of 2 (category_id + metadata), regardless of the JSON path length.
  • Path depth (MAX_JSON_QUERY_DEPTH, default 10): Limits the number of segments allowed in the path argument. For example, json(category_id.metadata, a[0].c.d.e.f.g.h.i.j) has a path depth of 10 and is allowed by default; adding one more segment would exceed the limit.
Exceeding either of these limits will result in an error.

SDK Type Safety

The SDK enforces that the field argument must be a json typed field from your schema, using a non-json field will result in a TypeScript error. The output alias is automatically typed as JsonValue | null, with no casting required.

Within the fields array, the SDK also provide partial autocomplete for the json() expression. For each json typed field in your schema, the IDE offers json(fieldName, as a completion, positioning the cursor ready for the path argument. This works via TypeScript's template-literal completion (TypeScript >= 4.7). The path argument is a free string with no completion hints.

import { createDirectus, readItems, rest } from "@directus/sdk";

interface Article {
  id: number;
  title: string;
  metadata: "json" | null; // type literal 'json' tells the SDK this is a json field
}

interface Schema {
  articles: Article[];
}

const client = createDirectus<Schema>("https://directus.example.com").with(
  rest(),
);

// valid: metadata is a json field; metadata_color_json is typed as JsonValue | null
readItems("articles", { fields: ["json(metadata, color)"] });

// type error: title is a string field, not json
readItems("articles", { fields: ["json(title, color)"] });

The alias rule follows the expected REST response format. For a relational field, the extracted alias appears typed on the related item (e.g. items[0].category_id.metadata_color_json).

Alias Typing Requires Literal Field Arrays Alias typing only works when the fields array is an inline literal or typed as const. If the array is built dynamically at runtime, TypeScript widens it to string[] and the aliases are not present in the inferred return type.

The _json Filter Operator

The _json operator filters items by values inside a JSON field. It accepts an object mapping JSON paths to standard filter operators, letting you compare specific keys or array elements without loading the full document.

_json is only valid on json typed fields.

Syntax

{ "field": { "_json": { "path": { "_operator": value } } } }

In GraphQL, input-object keys must be valid identifiers, so paths containing dots, brackets, or starting with [ must be passed as a typed variable (see Paths with Dots or Brackets).

Supported Inner Operators

The _json operator supports all standard filter operators except the following:

CategoryOperators
JSON_json
Geometric_intersects, _intersects_bbox
Regex_regex
Relational_some, _none

Basic Example

Filter articles where the color key inside the metadata JSON field equals "blue".

import { createDirectus, rest, readItems } from "@directus/sdk";
const directus = createDirectus("https://directus.example.com").with(rest());

const result = await directus.request(
  readItems("articles", {
    filter: {
      metadata: {
        _json: { color: { _eq: "blue" } },
      },
    },
  }),
);

Response:

{
  "data": [
    { "id": 1, "title": "An Article" },
    { "id": 4, "title": "Another Article" }
  ]
}

Multiple Path Conditions

Combine several path conditions inside a single _json object.

const result = await directus.request(
  readItems("articles", {
    filter: {
      metadata: {
        _json: {
          color: { _eq: "red" },
          brand: { _in: ["BrandX", "BrandY"] },
          level: { _gte: 3 },
        },
      },
    },
  }),
);

Response:

{
  "data": [{ "id": 7, "title": "Premium Red Item" }]
}

Paths with Dots or Brackets

Path keys with dots (settings.theme), bracket indices (tags[0]), or paths starting with [ are plain strings in REST and the SDK. In GraphQL, input-object keys must be valid identifiers, so pass the _json value as a typed variable instead.

const result = await directus.request(
  readItems("articles", {
    filter: {
      metadata: {
        _json: {
          "settings.theme": { _eq: "dark" },
          "tags[0]": { _eq: "electronics" },
        },
      },
    },
  }),
);

Response:

{
  "data": [{ "id": 2, "title": "Dark Mode Electronics Review" }]
}

Relational JSON Filtering

_json is nested under relational keys in the same way as other filters. To filter a JSON field on a related item, place _json under the relevant relation name.

const result = await directus.request(
  readItems("articles", {
    filter: {
      category_id: {
        metadata: {
          _json: { color: { _eq: "blue" } },
        },
      },
    },
  }),
);

Response:

{
  "data": [
    {
      "id": 1,
      "title": "An Article",
      "category_id": { "name": "News" }
    }
  ]
}

Combining Multiple Conditions

Combine multiple _json filters at the top level using _and or _or.

const result = await directus.request(
  readItems("articles", {
    filter: {
      _and: [
        { metadata: { _json: { color: { _eq: "blue" } } } },
        { metadata: { _json: { size: { _gt: 10 } } } },
      ],
    },
  }),
);

Response:

{
  "data": [{ "id": 3, "title": "Large Blue Article" }]
}

Conditions can also be grouped within the _json operator using _and or _or:

{
  "metadata": {
    "_json": {
      "_and": [{ "color": { "_eq": "blue" } }, { "size": { "_gt": 10 } }]
    }
  }
}

Dynamic Variables

Dynamic filter variables (e.g. $CURRENT_USER, $NOW etc) are supported within _json values. These variables are resolved before the filter is executed, allowing them to be used in permission rules and standard queries.

Database-Specific Notes

PostgreSQL

PostgreSQL returns JSON scalar values as text. For numeric comparisons in _json, Directus automatically casts values to a numeric type when the filter input is a number or number array, ensuring operators (e.g. _gt, _lt, _between etc) work as expected. If an expected numeric comparison is set with a string value (e.g. {"version":{"_gt":"9"}}), the comparison is instead performed lexicographically. Use numeric literals to ensure numeric comparison.

SQLite

SQLite will return 0 / 1 instead of boolean values when the resolved path is a boolean.

MSSQL

Scalar values are always returned as strings (NVARCHAR), even if the original JSON value is a number or boolean. For example, a JSON integer 42 is returned as "42". Applications should perform any type coercion as needed.

Oracle

Like MSSQL, Oracle returns scalar values as strings, regardless of the original JSON type being a number or boolean. For example, a JSON number 3.14 is returned as "3.14".

Get once-a-month release notes & real‑world code tips...no fluff. 🐰