JSON Queries
Directus provides two complementary tools for working with JSON fields in queries:
- The
json(field, path)function extracts a specific value from a JSON document. It can be used in thefields,sort, andaliasquery parameters. - The
_jsonfilter operator filters items by values inside a JSON document without loading the full document. Use it in thefilterquery parameter.
Both use the same path notation and work across REST, GraphQL, and the TypeScript SDK.
Path Notation
Paths use dot notation for object keys and bracket notation for array indices.
| Pattern | Example | Meaning |
|---|---|---|
key | color | Top-level key |
a.b.c | settings.theme.color | Nested keys |
[n] | tags[0] | Array element at index n |
a[n].b | items[0].name | Mixed object/array access |
Examples:
json(metadata, color) → top-level key
json(metadata, settings.theme) → nested object
json(data, items[0].name) → array element property
json(data, [0]) → first element of a top-level array
The following path syntaxes are not supported and return an error in both the function and filter operator:
| Expression | Example |
|---|---|
| Empty brackets (wildcard) | items[] |
[*] wildcard | items[*].name |
* glob | items.* |
| JSONPath predicates | items[?(@.price > 10)] |
@ current node | @.name |
$ root | $.name |
Object Keys with Special Characters
The path syntax uses . as a separator between key segments and has no escape mechanism. Object keys that contain dots, spaces, or other special characters cannot be reached. For example, a key "first.name" is interpreted as nested access to key first, then key name.
The json(field, path) Function
The json(field, path) function extracts the value from the specified path in a JSON field. It can be used anywhere 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 instead.Syntax
json(field, path)
fieldis the name of a JSON column in the collection (or a relational path to one).pathis a dot-and-bracket notation path to the value you want to extract from within the JSON document.
Both arguments are required and separated by a comma.
In GraphQL, each json-typed field exposes a json(path: String!) sub-field inside {fieldName}_func. The path argument is required. The return type is JSON (any scalar, object, or array). {fieldName}_func already exists for the count sub-field; json sits alongside it in the same selection.
The TypeScript SDK accepts json(fieldName, path) strings in the fields array. The first argument is constrained by TypeScript to fields typed as json in your schema. An invalid field name produces a type error. The SDK computes the response alias type automatically from the literal string, so extracted values are fully typed.
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
Special characters in the path ([, ], .) are replaced 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 |
In GraphQL, the extracted value is returned inside {fieldName}_func.json. When requesting multiple paths from the same field, use GraphQL field aliases to distinguish them.
Basic Example
GET /items/articles?fields=id,title,json(metadata, color)
query {
articles {
id
title
metadata_func {
json(path: "color")
}
}
}
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"
}
]
}
{
"data": {
"articles": [
{
"id": 1,
"title": "An Article",
"metadata_func": { "json": "blue" }
}
]
}
}
Multiple Paths
Extract multiple values from the same JSON field in a single request. In GraphQL, use field aliases on the json sub-field to distinguish each extraction.
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]")
}
}
}
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"
}
]
}
{
"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.
GET /items/articles?fields=id,json(metadata, dimensions),json(metadata, tags)
query {
articles {
id
metadata_func {
dimensions: json(path: "dimensions")
tags: json(path: "tags")
}
}
}
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"]
}
]
}
{
"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 goes inside 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 any other requested fields from that relation. Multiple json(field, path) extractions from the same relation are grouped under the same relational key.
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")
}
}
}
}
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"
}
}
]
}
{
"data": {
"articles": [
{
"id": 1,
"title": "An Article",
"category_id": {
"name": "News",
"metadata_func": { "color": "blue" }
}
}
]
}
}
One-to-Many (O2M)
For O2M relations, each related item returns its own extracted value. The response contains an array of objects, each with the extracted key.
GET /items/articles/1?fields=id,json(comments.data, type)
query {
articles_by_id(id: 1) {
id
comments {
data_func {
json(path: "type")
}
}
}
}
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" }
]
}
}
{
"data": {
"articles_by_id": {
"id": 1,
"comments": [
{ "data_func": { "json": "comment" } },
{ "data_func": { "json": "review" } }
]
}
}
}
Many-to-Any (M2A)
For M2A relations, use the standard Directus collection scope syntax inside the first argument: json(relation.item:collection_name.json_field, path)
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")
}
}
}
}
}
}
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"
}
}
]
}
}
{
"data": {
"shapes_by_id": {
"id": 1,
"children": [
{
"item": {
"metadata_func": { "color": "red" }
}
}
]
}
}
}
Depth Limits
json(field, path) enforces two separate depth limits, calculated independently:
- Relational depth (
MAX_RELATIONAL_DEPTH, default10) limits how deep the relational path in the first argument can go.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 how many segments the JSON path itself can contain.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; one more segment exceeds the limit.
Exceeding either limit returns an error.
TypeScript SDK Type Safety
The SDK enforces that the first argument of json() must be a json-typed field. Non-json fields produce a TypeScript error. The output alias is typed automatically as JsonValue | null with no cast needed.
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 is {field}_{path}_json with ., [, and ] replaced by _. For a relational field, the extracted alias appears typed on the related item (for example, 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.When typing inside the fields array, the SDK provides partial autocomplete for the json() function. 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). Only json-typed fields appear as suggestions; the path argument is a free string with no completion hints.
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
The _json value is an object where each key is a JSON path and each value is a standard filter operator object.
{ "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
| Category | Operators |
|---|---|
| Equality | _eq, _neq, _ieq, _nieq |
| Null | _null, _nnull |
| Set | _in, _nin |
| String | _contains, _ncontains, _icontains, _nicontains |
| Prefix / Suffix | _starts_with, _ends_with (plus _i variants) |
| Numeric | _gt, _gte, _lt, _lte, _between, _nbetween |
| Empty | _empty, _nempty |
Basic Example
Filter articles where the color key inside the metadata JSON field equals "blue".
GET /items/articles
?filter={"metadata":{"_json":{"color":{"_eq":"blue"}}}}
query {
articles(filter: { metadata: { _json: { color: { _eq: "blue" } } } }) {
id
title
}
}
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.
GET /items/articles
?filter={"metadata":{"_json":{"color":{"_eq":"red"},"brand":{"_in":["BrandX","BrandY"]},"level":{"_gte":3}}}}
query {
articles(
filter: {
metadata: {
_json: {
color: { _in: ["red", "blue"] }
brand: { _nnull: true }
description: { _contains: "premium" }
}
}
}
) {
id
title
}
}
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.
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 }
# }
# }
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 nests under relational keys the same way other filters do. To filter on a JSON field belonging to a related item, nest _json under the relation name.
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
}
}
}
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 with _and or _or.
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: { level: { _gte: 3 } } } }
]
}
) {
id
title
}
}
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" }
]
}
You can also group conditions inside a single _json value using _and or _or:
{
"metadata": {
"_json": {
"_and": [
{ "color": { "_eq": "blue" } },
{ "size": { "_gt": 10 } }
]
}
}
}
Dynamic Variables
Dynamic filter variables like $CURRENT_USER and $NOW work inside _json inner values. They are resolved before the filter runs, so they apply in permission rules and regular queries.
Database-Specific Notes
PostgreSQL
PostgreSQL extracts JSON scalars as text. For _json numeric comparisons, Directus automatically casts to a numeric type when the filter value is a number or an array of numbers, so _gt, _lt, _between, and related operators work correctly. If you supply a numeric comparison with a string value (for example {"version":{"_gt":"9"}}), the comparison remains lexicographic. Use a numeric literal to get numeric comparison.
SQLite
SQLite can return 0/1 instead of boolean values when the path resolves to a boolean.
MSSQL
Always returns scalar values as strings (NVARCHAR), even when the original JSON value is a number or boolean. For example, a JSON integer 42 is returned as the string "42". Your application should perform type coercion as needed.
Oracle
Similar to MSSQL, Oracle returns scalar values as strings, regardless of the original JSON type (number, boolean, etc.). A JSON number 3.14 is returned as "3.14".
Get once-a-month release notes & real‑world code tips...no fluff. 🐰