Advanced Querying
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.
| Pattern | Example | Meaning |
|---|---|---|
key | color | Top-level object key |
a.b.c | settings.theme.color | Nested object key |
[n] | tags[0] | Array element at index n |
a[n].b | items[0].name | Mixed object/array access |
Examples:
json(metadata, settings.theme)
{
"metadata": {
"_json": {
"settings.theme": {
"_eq":"blue"
}
}
}
}
Unsupported Path Expressions
The following path syntaxes are not supported and and will result in an error if used
| Expression | Example |
|---|---|
| Empty brackets (wildcard) | items[] |
[*] wildcard | items[*].name |
* glob | items.* |
| JSONPath predicates | items[?(@.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.
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.
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.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 field | Response 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 |
{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)"],
}),
);
GET /items/articles?fields=id,title,json(metadata, color)
query {
articles {
id
title
metadata_func {
json(path: "color")
}
}
}
Response:
{
"data": [
{
"id": 1,
"title": "An Article",
"metadata_color_json": "blue"
}
]
}
{
"data": {
"articles": [
{
"id": 1,
"title": "An Article",
"metadata_func": { "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])",
],
}),
);
GET /items/articles?fields=id,json(metadata, color),json(metadata, settings.theme),json(metadata, tags[0])
query {
articles {
id
metadata_func {
color: json(path: "color")
theme: json(path: "settings.theme")
firstTag: json(path: "tags[0]")
}
}
}
Response:
{
"data": [
{
"id": 1,
"metadata_color_json": "blue",
"metadata_settings_theme_json": "dark",
"metadata_tags_0_json": "featured"
}
]
}
{
"data": {
"articles": [
{
"id": 1,
"metadata_func": {
"color": "blue",
"theme": "dark",
"firstTag": "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.
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)"],
}),
);
GET /items/articles?fields=id,json(metadata, dimensions),json(metadata, tags)
query {
articles {
id
metadata_func {
dimensions: json(path: "dimensions")
tags: json(path: "tags")
}
}
}
Response:
{
"data": [
{
"id": 1,
"metadata_dimensions_json": { "width": 100, "height": 50 },
"metadata_tags_json": ["featured", "new"]
}
]
}
{
"data": {
"articles": [
{
"id": 1,
"metadata_func": {
"dimensions": { "width": 100, "height": 50 },
"tags": ["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)"] }],
}),
);
GET /items/articles?fields=id,title,category_id.name,json(category_id.metadata, color)
query {
articles {
id
title
category_id {
name
metadata_func {
color: json(path: "color")
}
}
}
}
Response:
{
"data": [
{
"id": 1,
"title": "An Article",
"category_id": {
"name": "News",
"metadata_color_json": "blue"
}
}
]
}
{
"data": {
"articles": [
{
"id": 1,
"title": "An Article",
"category_id": {
"name": "News",
"metadata_func": { "color": "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)"] }],
}),
);
GET /items/articles/1?fields=id,json(comments.data, type)
query {
articles_by_id(id: 1) {
id
comments {
data_func {
json(path: "type")
}
}
}
}
Response:
{
"data": {
"id": 1,
"comments": [
{ "data_type_json": "comment" },
{ "data_type_json": "review" }
]
}
}
{
"data": {
"articles_by_id": {
"id": 1,
"comments": [
{ "data_func": { "json": "comment" } },
{ "data_func": { "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)"],
},
},
],
},
],
}),
);
GET /items/shapes/1?fields=id,json(children.item:circles.metadata, color)
query {
shapes_by_id(id: 1) {
id
children {
item {
... on circles {
metadata_func {
json(path: "color")
}
}
}
}
}
}
Response:
{
"data": {
"id": 1,
"children": [
{
"item": {
"metadata_color_json": "red"
}
}
]
}
}
{
"data": {
"shapes_by_id": {
"id": 1,
"children": [
{
"item": {
"metadata_func": { "color": "red" }
}
}
]
}
}
}
Depth Limits
json(field, path) enforces two independent depth limits:
- Relational depth (
MAX_RELATIONAL_DEPTH, default10): Limits how deeply relational selections can go in thefieldargument. 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, default10): Limits the number of segments allowed in thepathargument. 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.
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).
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:
| Category | Operators |
|---|---|
| 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" } },
},
},
}),
);
GET /items/articles
?filter={"metadata":{"_json":{"color":{"_eq":"blue"}}}}
query {
articles(filter: { metadata: { _json: { color: { _eq: "blue" } } } }) {
id
title
}
}
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 },
},
},
},
}),
);
GET /items/articles
?filter={"metadata":{"_json":{"color":{"_eq":"red"},"brand":{"_in":["BrandX","BrandY"]},"level":{"_gte":3}}}}
query {
articles(
filter: {
metadata: {
_json: {
color: { _eq: "red" }
brand: { _in: ["BrandX", "BrandY"] }
level: { _gte: 3 }
}
}
}
) {
id
title
}
}
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" },
},
},
},
}),
);
GET /items/articles
?filter={"metadata":{"_json":{"settings.theme":{"_eq":"dark"},"tags[0]":{"_eq":"electronics"}}}}
query FilterByNestedPath($jsonFilter: JSON) {
articles(filter: { metadata: { _json: $jsonFilter } }) {
id
title
}
}
# Variables:
# {
# "jsonFilter": {
# "settings.theme": { "_eq": "dark" },
# "tags[0]": { "_eq": "electronics" },
# "[0].test": { "_null": false }
# }
# }
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" } },
},
},
},
}),
);
GET /items/articles
?filter={"category_id":{"metadata":{"_json":{"color":{"_eq":"blue"}}}}}
query {
articles(
filter: { category_id: { metadata: { _json: { color: { _eq: "blue" } } } } }
) {
id
title
category_id {
name
}
}
}
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 } } } },
],
},
}),
);
GET /items/articles
?filter={"_and":[{"metadata":{"_json":{"color":{"_eq":"blue"}}}},{"metadata":{"_json":{"size":{"_gt":10}}}}]}
query {
articles(
filter: {
_and: [
{ metadata: { _json: { color: { _eq: "blue" } } } }
{ metadata: { _json: { size: { _gt: 10 } } } }
]
}
) {
id
title
}
}
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. 🐰