diff --git a/content/_partials/json-function.md b/content/_partials/json-function.md deleted file mode 100644 index 37e2bb782..000000000 --- a/content/_partials/json-function.md +++ /dev/null @@ -1,215 +0,0 @@ -## The `json(field, path)` Function - -The `json(field, path)` function extracts the value from the specified path in a JSON field and returns it as a separate field in the query response. It is used in the `fields` query parameter alongside regular field names and other field functions. - - -::callout{icon="material-symbols:warning-rounded" color="warning"} - -**Supported Paramaters** -the `json(field, path)` function is currently only supported for use in the `fields` query parameter. - -:: - -### Syntax - -``` -json(field, path) -``` - -- **`field`** — the name of a JSON column in the collection (or a relational path to one, see [Relational Queries](#relational-queries)). -- **`path`** — 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. - -### 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 -``` - -### Response Format - -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. For example: - -| 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` | - -#### Example Request and Response - -```http -GET /items/articles?fields=id,title,json(metadata, color)&sort=title -``` - -```json -{ - "data": [ - { - "id": 1, - "title": "An Article", - "metadata_color_json": "blue" - } - ] -} -``` - -### 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) - -``` -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. - -```http -GET /items/articles?fields=id,title,category_id.name,json(category_id.metadata, color) -``` - -```json -{ - "data": [ - { - "id": 1, - "title": "An Article", - "category_id": { - "name": "Tech", - "metadata_color_json": "blue" - } - } - ] -} -``` - -Multiple `json(field, path)` extractions from the same relation are grouped under the same relational key: - -```http -GET /items/articles?fields=id,json(category_id.metadata, color),json(category_id.metadata, icon) -``` - -```json -{ - "data": [ - { - "category_id": { - "metadata_color_json": "blue", - "metadata_icon_json": "laptop" - } - } - ] -} -``` - -#### One-to-Many (O2M) - -For O2M relations, each related item returns its own extracted value. The response is an array of objects, each containing the extracted key. - -```http -GET /items/articles/1?fields=id,json(comments.data, type) -``` - -```json -{ - "data": { - "id": 1, - "comments": [ - { "data_type_json": "review" }, - { "data_type_json": "feedback" }, - { "data_type_json": "question" } - ] - } -} -``` - -#### 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) -``` - -```http -GET /items/shapes/1?fields=id,json(children.item:circles.metadata, color) -``` - -### Relational Depth Limit - -`json(field, path)` will enforce a maximum relational depth (`MAX_RELATIONAL_DEPTH`, default `10`) limit for the `field` argument. This depth is calculated irrespective of the Path depth limit mentioned below - -``` -json(category_id.metadata, a.b.c.d.e) -``` -This has a relational depth of **2** (`category_id` + `metadata`), regardless of how many segments are in the JSON path `a.b.c.d.e`. - -Exceeding the relational depth will return an error. - -### JSON Path Depth Limit - -In addition to a relation depth, `json(field, path)` will also enforce a path depth limit (`MAX_JSON_QUERY_DEPTH`, default `10`). This depth is calculated irrespective of the relational depth. - -``` -json(category_id.metadata, a[0].c.d.e.f.g.h.i.j) -``` - -The above example has a path depth of 10 and is allowed by default; adding one more segment exceeds the limit. - -Exceeding the path depth limit returns an error. - -### Unsupported Path Expressions - -The following path syntaxes are **not supported** and will return an error: - -| 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 `json(field, path)` path syntax uses `.` as a separator between key segments. There is no escape mechanism for object keys that themselves contain dots, spaces, or other special characters. For example, if your JSON has a key `"first.name"`, there is no way to express that in the path — `json(data, first.name)` would be interpreted as nested access to key `first`, then key `name`. - -Similarly, because MySQL and MariaDB path conversion uses dot-notation (`$.key.subkey`), keys containing characters that are special in that context (e.g., spaces) may not be reachable. PostgreSQL's parameterized `->?` approach is more permissive for unusual key names, but the input path format still does not provide an escaping mechanism. - -### Database-Specific Exceptions - -**SQLite** - -- SQLite can return `0`/`1` isntead of `boolean` values. - -**MSSQL** - -- Will always returns scalar values as **strings (`NVARCHAR`)**, even when the original JSON value is a number or boolean. For example, a JSON integer `42` will be returned as the string `"42"`. Your application should perform type coercion as needed. - -**Oracle** - -- Similar to MSSQL will also return scalar values as **strings**, regardless of the original JSON type (number, boolean, etc.). A JSON number `3.14` will be returned as `"3.14"`. diff --git a/content/_partials/query-functions.md b/content/_partials/query-functions.md index 351d3a2ad..434d897f5 100644 --- a/content/_partials/query-functions.md +++ b/content/_partials/query-functions.md @@ -13,4 +13,3 @@ The syntax for using a function is `function(field)`. | `minute` | Extract the minute from a datetime/time/timestamp field | | `second` | Extract the second from a datetime/time/timestamp field | | `count` | Extract the number of items from a JSON array or relational field | -| `json` | Extract a specific value from a JSON field using path notation | diff --git a/content/guides/04.connect/2.filter-rules.md b/content/guides/04.connect/2.filter-rules.md index a32268d86..08610d737 100644 --- a/content/guides/04.connect/2.filter-rules.md +++ b/content/guides/04.connect/2.filter-rules.md @@ -36,6 +36,7 @@ Filters are used in permissions, validations, and automations, as well as throug | `_nbetween` | Is not between two values (inclusive) | | `_empty` | Is empty (`null` or falsy) | | `_nempty` | Isn't empty (`null` or falsy) | +| `_json` [5] | Compare values inside a JSON document | | `_intersects` [2] | Intersects a point | | `_nintersects` [2] | Doesn't intersect a point | | `_intersects_bbox` [2] | Intersects a bounding box | @@ -47,7 +48,8 @@ Filters are used in permissions, validations, and automations, as well as throug [1] Compared value is not strictly typed for numeric values, allowing comparisons between numbers and their string representations.
[2] Only available on geometry fields.
[3] Only available in validation permissions.
-[4] Only available on One to Many relationship fields. +[4] Only available on One to Many relationship fields.
+[5] Only available on JSON fields, see the [JSON Querying Quickstart](/guides/connect/json/quickstart) for usage and examples. ## Filter Syntax diff --git a/content/guides/04.connect/3.query-parameters.md b/content/guides/04.connect/3.query-parameters.md index a27e30616..4169ab9a5 100644 --- a/content/guides/04.connect/3.query-parameters.md +++ b/content/guides/04.connect/3.query-parameters.md @@ -8,11 +8,12 @@ Most Directus API endpoints can use global query parameters to alter the data th ## Fields -Specify which fields are returned. This parameter also supports dot notation to request nested relational fields, and wildcards (*) to include all fields at a specific depth. +Specify which fields are returned. This parameter also supports dot notation to request nested relational fields, and wildcards (\*) to include all fields at a specific depth. ::code-group + ```http [REST] GET /items/posts ?fields=first_name,last_name,avatar.description @@ -24,20 +25,21 @@ Use native GraphQL queries. ```json [SDK] { - "fields": ["first_name", "last_name", { "avatar": ["description"] }] + "fields": ["first_name", "last_name", { "avatar": ["description"] }] } ``` + :: ::callout{icon="material-symbols:info-outline"} **Examples** -| Value | Description | +| Value | Description | | ---------------------- | ------------------------------------------------------------------ | -| `first_name,last_name` | Return only the `first_name` and `last_name` fields. | -| `title,author.name` | Return `title` and the related `author` item's `name` field. | -| `*` | Return all fields. | -| `*.*` | Return all fields and all immediately related fields. | -| `*,images.*` | Return all fields and all fields within the `images` relationship. | +| `first_name,last_name` | Return only the `first_name` and `last_name` fields. | +| `title,author.name` | Return `title` and the related `author` item's `name` field. | +| `*` | Return all fields. | +| `*.*` | Return all fields and all immediately related fields. | +| `*,images.*` | Return all fields and all fields within the `images` relationship. | :: ::callout{icon="material-symbols:info-outline"} @@ -53,17 +55,19 @@ As Many to Any (M2A) fields have nested data from multiple collections, you are **Example** In an `posts` collection there is a Many to Any field called `sections` that points to `headings`, `paragraphs`, and `videos`. Different fields should be fetched from each related collection. - ::code-group - ```http [REST] - GET /items/posts - ?fields[]=title - &fields[]=sections.item:headings.title - &fields[]=sections.item:headings.level - &fields[]=sections.item:paragraphs.body - &fields[]=sections.item:videos.source - ``` + ::code-group + ```http [REST] + +GET /items/posts +?fields[]=title +&fields[]=sections.item:headings.title +&fields[]=sections.item:headings.level +&fields[]=sections.item:paragraphs.body +&fields[]=sections.item:videos.source + +````` - ```graphql [GraphQL] +````graphql [GraphQL] # Use can use native GraphQL Union types. query { @@ -127,32 +131,33 @@ GET /items/posts GET /items/posts ?filter={ "title": { "_eq": "Hello" }} -``` +````` ```graphql [GraphQL] query { - posts(filter: { title: { _eq: "Hello" } }) { - id - } + posts(filter: { title: { _eq: "Hello" } }) { + id + } } # Attribute names in GraphQL cannot contain the `:` character. If you are filtering Many to Any fields, you will need to replace it with a double underscore. ``` ```js [SDK] -import { createDirectus, rest, readItems } from '@directus/sdk'; -const directus = createDirectus('https://directus.example.com').with(rest()); +import { createDirectus, rest, readItems } from "@directus/sdk"; +const directus = createDirectus("https://directus.example.com").with(rest()); const result = await directus.request( - readItems('posts', { - filter: { - title: { - _eq: 'Hello', - }, - }, - }) + readItems("posts", { + filter: { + title: { + _eq: "Hello", + }, + }, + }), ); ``` + :: ## Search @@ -168,22 +173,23 @@ GET /items/posts ```graphql [GraphQL] query { - posts(search: "Directus") { - id - } + posts(search: "Directus") { + id + } } ``` ```js [SDK] -import { createDirectus, rest, readItems } from '@directus/sdk'; -const directus = createDirectus('https://directus.example.com').with(rest()); +import { createDirectus, rest, readItems } from "@directus/sdk"; +const directus = createDirectus("https://directus.example.com").with(rest()); const result = await directus.request( - readItems('posts', { - search: 'Directus', - }) + readItems("posts", { + search: "Directus", + }), ); ``` + :: ## Sort @@ -193,6 +199,7 @@ const result = await directus.request( What fields to sort results by. Sorting defaults to ascending, but appending a `-` will reverse this. Fields are prioritized by the order in the parameter. The dot notation is used to sort with values of related fields. ::code-group + ```http [REST] GET /items/posts ?sort=sort,-date_created,author.name @@ -200,22 +207,23 @@ GET /items/posts ```graphql [GraphQL] query { - posts(sort: ["sort", "-date_created", "author.name"]) { - id - } + posts(sort: ["sort", "-date_created", "author.name"]) { + id + } } ``` ```js [SDK] -import { createDirectus, rest, readItems } from '@directus/sdk'; -const directus = createDirectus('https://directus.example.com').with(rest()); +import { createDirectus, rest, readItems } from "@directus/sdk"; +const directus = createDirectus("https://directus.example.com").with(rest()); const result = await directus.request( - readItems('posts', { - sort: ['sort', '-date_created', 'author.name'], - }) + readItems("posts", { + sort: ["sort", "-date_created", "author.name"], + }), ); ``` + :: ## Limit @@ -223,6 +231,7 @@ const result = await directus.request( Set the maximum number of items that will be returned. The default limit is set to `100`. `-1` will return all items. ::code-group + ```http [REST] GET /items/posts ?limit=50 @@ -230,22 +239,23 @@ GET /items/posts ```graphql [GraphQL] query { - posts(limit: 50) { - id - } + posts(limit: 50) { + id + } } ``` ```js [SDK] -import { createDirectus, rest, readItems } from '@directus/sdk'; -const directus = createDirectus('https://directus.example.com').with(rest()); +import { createDirectus, rest, readItems } from "@directus/sdk"; +const directus = createDirectus("https://directus.example.com").with(rest()); const result = await directus.request( - readItems('posts', { - limit: 50, - }) + readItems("posts", { + limit: 50, + }), ); ``` + :: ::callout{icon="material-symbols:info-outline"} @@ -260,6 +270,7 @@ The maximum number of items that can be requested on the API can be configured u Skip the specified number of items in the response. This parameter can be used for pagination. ::code-group + ```http [REST] GET /items/posts ?offset=100 @@ -267,22 +278,23 @@ GET /items/posts ```graphql [GraphQL] query { - posts(offset: 100) { - id - } + posts(offset: 100) { + id + } } ``` ```js [SDK] -import { createDirectus, rest, readItems } from '@directus/sdk'; -const directus = createDirectus('https://directus.example.com').with(rest()); +import { createDirectus, rest, readItems } from "@directus/sdk"; +const directus = createDirectus("https://directus.example.com").with(rest()); const result = await directus.request( - readItems('posts', { - offset: 100, - }) + readItems("posts", { + offset: 100, + }), ); ``` + :: ## Page @@ -290,6 +302,7 @@ const result = await directus.request( An alternative to `offset`. Returned values are the value of `limit` multiplied by `page`. The first page is `1`. ::code-group + ```http [REST] GET /items/posts ?page=2 @@ -297,22 +310,23 @@ GET /items/posts ```graphql [GraphQL] query { - posts(page: 2) { - id - } + posts(page: 2) { + id + } } ``` ```js [SDK] -import { createDirectus, rest, readItems } from '@directus/sdk'; -const directus = createDirectus('https://directus.example.com').with(rest()); +import { createDirectus, rest, readItems } from "@directus/sdk"; +const directus = createDirectus("https://directus.example.com").with(rest()); const result = await directus.request( - readItems('posts', { - page: 2, - }) + readItems("posts", { + page: 2, + }), ); ``` + :: ## Aggregate @@ -332,6 +346,7 @@ Aggregate functions allow you to perform calculations on a set of values, return | `countAll` | Equivalent to `?aggregate[count]=*` (GraphQL only) | ::code-group + ```http [REST] GET /items/posts ?aggregate[count]=* @@ -339,22 +354,23 @@ GET /items/posts ```graphql [GraphQL] query { - posts_aggregated { - countAll - } + posts_aggregated { + countAll + } } ``` ```js [SDK] -import { createDirectus, rest, aggregate } from '@directus/sdk'; -const directus = createDirectus('https://directus.example.com').with(rest()); +import { createDirectus, rest, aggregate } from "@directus/sdk"; +const directus = createDirectus("https://directus.example.com").with(rest()); const result = await directus.request( - aggregate('posts', { - aggregate: { count: '*' }, - }) + aggregate("posts", { + aggregate: { count: "*" }, + }), ); ``` + :: ## GroupBy @@ -364,6 +380,7 @@ Grouping allows for running aggregate functions based on a shared value, rather You can group by multiple fields simultaneously. Combined with the functions, this allows for aggregate reporting per year-month-date. ::code-group + ```http [REST] GET /items/posts ?aggregate[count]=views,comments @@ -384,18 +401,19 @@ query { ``` ```js [SDK] -import { createDirectus, rest, aggregate } from '@directus/sdk'; -const directus = createDirectus('https://directus.example.com').with(rest()); +import { createDirectus, rest, aggregate } from "@directus/sdk"; +const directus = createDirectus("https://directus.example.com").with(rest()); const result = await directus.request( - aggregate('posts', { + aggregate("posts", { aggregate: { - count: ['views', 'comments'] + count: ["views", "comments"], }, - groupBy: ['author', 'year(publish_date)'], - }) + groupBy: ["author", "year(publish_date)"], + }), ); ``` + :: ## Deep @@ -405,6 +423,7 @@ Deep allows you to set any of the other query parameters (except for [Fields](#f The nested query parameters are to be prefixed with an underscore. ::code-group + ```http [REST] // There are two available syntax: @@ -418,50 +437,53 @@ GET /items/posts ```graphql [GraphQL] # Natively supported by GraphQL. query { - posts { - translations(filter: { languages_code: { code: { _eq: "en-US" } } }) { - id - } - } + posts { + translations(filter: { languages_code: { code: { _eq: "en-US" } } }) { + id + } + } } ``` ```js [SDK] -import { createDirectus, rest, readItems } from '@directus/sdk'; -const directus = createDirectus('https://directus.example.com').with(rest()); +import { createDirectus, rest, readItems } from "@directus/sdk"; +const directus = createDirectus("https://directus.example.com").with(rest()); const result = await directus.request( - readItems('posts', { - deep: { - translations: { - _filter: { - languages_code: { - _eq: 'en-US', - }, - } - }, - }, - }) + readItems("posts", { + deep: { + translations: { + _filter: { + languages_code: { + _eq: "en-US", + }, + }, + }, + }, + }), ); ``` + :: ::callout{icon="material-symbols:info-outline"} **Example** Only get 3 related posts, with only the top rated comment nested: + ```json { - "deep": { - "related_posts": { - "_limit": 3, - "comments": { - "_sort": "rating", - "_limit": 1 - } - } + "deep": { + "related_posts": { + "_limit": 3, + "comments": { + "_sort": "rating", + "_limit": 1 + } + } } } ``` + :: ## Alias @@ -480,40 +502,43 @@ GET /items/posts ```graphql [GraphQL] # Natively supported by GraphQL. query { - posts { - dutch_translations: translations(filter: { code: { _eq: "nl-NL" } }) { - id - } + posts { + dutch_translations: translations(filter: { code: { _eq: "nl-NL" } }) { + id + } - all_translations: translations { - id - } - } + all_translations: translations { + id + } + } } ``` ```js [SDK] -import { createDirectus, rest, readItems } from '@directus/sdk'; -const directus = createDirectus('https://directus.example.com').with(staticToken()).with(rest()); +import { createDirectus, rest, readItems } from "@directus/sdk"; +const directus = createDirectus("https://directus.example.com") + .with(staticToken()) + .with(rest()); const result = await directus.request( - readItems('posts', { - alias: { - all_translations: 'translations', - dutch_translations: 'translations', - }, - deep: { - dutch_translations: { - _filter: { - code: { - _eq: 'nl-NL', - }, - }, - }, - }, - }) + readItems("posts", { + alias: { + all_translations: "translations", + dutch_translations: "translations", + }, + deep: { + dutch_translations: { + _filter: { + code: { + _eq: "nl-NL", + }, + }, + }, + }, + }), ); ``` + :: ::callout{icon="material-symbols:info-outline"} @@ -543,6 +568,8 @@ Queries a version of a record by version key when [content versioning](/guides/c The keys `published` and `draft` are reserved. Use `published` (or `main` for backward compatibility) to explicitly fetch the published base item. Use `draft` to fetch the global draft version. :: +::code-group + ```http [GET /items/posts/1] ?version=v1 ``` @@ -562,16 +589,21 @@ const directus = createDirectus("https://directus.example.com").with(rest()); const result = await directus.request( readItem("posts", { version: "v1", - }) + }), ); ``` +:: + ## VersionRaw Specifies to return relational delta changes as a [detailed output](https://directus.io/docs/guides/connect/relations#creating-updating-deleting) on a version record. -```http [GET /items/posts/1] -?version=v1&versionRaw=true +::code-group + +```http [REST] +GET /items/posts/1 + ?version=v1&versionRaw=true ``` ```graphql [GraphQL] @@ -590,15 +622,18 @@ const result = await directus.request( readItem("posts", { version: "v1", versionRaw: true, - }) + }), ); ``` +:: + ## Functions :partial{content="query-functions"} ::code-group + ```http [REST] GET /items/posts ?filter[year(date_published)][_eq]=1968 @@ -606,35 +641,40 @@ GET /items/posts ```graphql [GraphQL] query { - posts(filter: { date_published_func: { year: { _eq: 1968 } } }) { - id - } + posts(filter: { date_published_func: { year: { _eq: 1968 } } }) { + id + } } # Due to GraphQL name limitations, append `_func` at the end of the field name and use the function name as the nested field. ``` ```js [SDK] -import { createDirectus, rest, readItems } from '@directus/sdk'; -const directus = createDirectus('https://directus.example.com').with(rest()); +import { createDirectus, rest, readItems } from "@directus/sdk"; +const directus = createDirectus("https://directus.example.com").with(rest()); const result = await directus.request( - readItems('posts', { - filter: { - "year(date_published)": { - _eq: 1968 - } - }, - }) + readItems("posts", { + filter: { + "year(date_published)": { + _eq: 1968, + }, + }, + }), ); ``` + :: -:partial{content="json-function"} +### The JSON Function + +Extract a specific value from a JSON field and return it as a separate field in the response. See [Using the `json(field, path)` function](/guides/connect/json/quickstart#using-the-json-function) for full path syntax and examples. + +This function cannot be used in `filter`. To filter on JSON values, use the [`_json` filter operator](/guides/connect/json/quickstart#filtering-with-_json) instead. ## Backlink -When backlink is set to `false`, the API will exclude reverse relations during `*.*` wildcard field expansion to prevent circular references and reduce duplicate data in responses. +When backlink is set to `false`, the API will exclude reverse relations during `*.*` wildcard field expansion to prevent circular references and reduce duplicate data in responses. The backlink parameter defaults to `true`, so you need to explicitly set it to `false` to enable the filtering behavior. @@ -659,7 +699,7 @@ const directus = createDirectus("https://directus.example.com").with(rest()); const result = await directus.request( readItems("posts", { backlink: false, - }) + }), ); ``` @@ -744,4 +784,4 @@ The articles collection consists of a many-to-one relation to Users called `auth } ``` -:: \ No newline at end of file +:: diff --git a/content/guides/04.connect/7.json/1.quickstart.md b/content/guides/04.connect/7.json/1.quickstart.md new file mode 100644 index 000000000..72c03bd39 --- /dev/null +++ b/content/guides/04.connect/7.json/1.quickstart.md @@ -0,0 +1,172 @@ +--- +stableId: 6f7ab223-9812-4f22-b386-bab6189f6639 +title: Quickstart +description: Quickstart for extracting and filtering values inside JSON fields with the json() function and _json filter operator. +--- + +Directus provides two ways of working with JSON fields in queries: + +- [`json(field, path)`](/guides/connect/json/quickstart#using-the-json-function): A selection function to extract a value from a JSON document. It can be used in the `fields`, `sort`, and `alias` parameters. +- [`_json`](/guides/connect/json/quickstart#filtering-with-_json): A filter operator that lets you filter records based on values within a JSON document. It can be used within the `filter` parameter. + +Both use the same path notation and work across REST, GraphQL, and the SDK. + +## Using the `json()` function + +**Function Syntax:** + +``` +json(field, path) +``` + +**Example:** + +Extract the `color` key from a `metadata` JSON field: + +::code-group + +```http [REST] +GET /items/articles?fields=id,title,json(metadata, color) +``` + +```js [SDK] +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)"], + }), +); +``` + +```graphql [GraphQL] +query { + articles { + id + title + metadata_func { + json(path: "color") + } + } +} +``` + +:: + +Response: + +::code-group + +```json [REST / SDK] +{ + "data": [ + { + "id": 1, + "title": "An Article", + "metadata_color_json": "blue" + } + ] +} +``` + +```json [GraphQL] +{ + "data": { + "articles": [ + { + "id": 1, + "title": "An Article", + "metadata_func": { "json": "blue" } + } + ] + } +} +``` + +:: + +For REST and SDK, the extracted value is returned under the alias `{field}_{path}_json` with `.`, `[`, and `]` replaced by underscores. + +## Filtering with `_json` + +**Operator Syntax:** + +``` +{ + "field": { + "_json": { + "path": { + "_operator": value + } + } + } +} +``` + +**Example:** + +Find articles where the `color` key inside `metadata` equals `"blue"`: + +::code-group + +```http [REST] +GET /items/articles + ?filter={"metadata":{"_json":{"color":{"_eq":"blue"}}}} +``` + +```js [SDK] +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" } }, + }, + }, + }), +); +``` + +```graphql [GraphQL] +query { + articles(filter: { metadata: { _json: { color: { _eq: "blue" } } } }) { + id + title + } +} +``` + +:: + +Response: + +```json +{ + "data": [ + { "id": 1, "title": "An Article" }, + { "id": 4, "title": "Another Article" } + ] +} +``` + +Refer to the [Supported Inner Operations](/guides/connect/json/advanced-querying#supported-inner-operators) section for a list of available operators within the `_json` operator. + +## 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 | + +Wildcards (`*`, `[*]`) and other special characters are not currently supported. See the [Unsupported Path Expressions](/guides/connect/json/advanced-querying#unsupported-path-expressions) section for a complete list of unsupported characters. + +## More information + +For advanced usage details and additional examples, see [Advanced JSON Querying](/guides/connect/json/advanced-querying). diff --git a/content/guides/04.connect/7.json/2.advanced-querying.md b/content/guides/04.connect/7.json/2.advanced-querying.md new file mode 100644 index 000000000..d50c01de0 --- /dev/null +++ b/content/guides/04.connect/7.json/2.advanced-querying.md @@ -0,0 +1,937 @@ +--- +stableId: 26102b15-bee8-4e86-8f1f-4d74f1ea9cb0 +title: Advanced Querying +description: 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](/guides/connect/json/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:** + +::code-group + +```[Field Selection] +json(metadata, settings.theme) +``` + +```[Filtering] +{ + "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. + +::callout{icon="material-symbols:warning-rounded" color="warning"} +**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](#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. + +::callout{icon="material-symbols:info-outline" color="info"} +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. +:: + +::callout{icon="material-symbols:info-outline" color="info"} +The SDK supports a type safe `json(field, path)` expression within its `fields` array, see [SDK Type Safety](#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` | + +::callout{icon="material-symbols:warning-rounded" color="warning"} +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 + +::code-group + +```http [REST] +GET /items/articles?fields=id,title,json(metadata, color) +``` + +```graphql [GraphQL] +query { + articles { + id + title + metadata_func { + json(path: "color") + } + } +} +``` + +```js [SDK] +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: + +::code-group + +```json [REST / SDK] +{ + "data": [ + { + "id": 1, + "title": "An Article", + "metadata_color_json": "blue" + } + ] +} +``` + +```json [GraphQL] +{ + "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. + +::code-group + +```http [REST] +GET /items/articles?fields=id,json(metadata, color),json(metadata, settings.theme),json(metadata, tags[0]) +``` + +```graphql [GraphQL] +query { + articles { + id + metadata_func { + color: json(path: "color") + theme: json(path: "settings.theme") + firstTag: json(path: "tags[0]") + } + } +} +``` + +```js [SDK] +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: + +::code-group + +```json [REST / SDK] +{ + "data": [ + { + "id": 1, + "metadata_color_json": "blue", + "metadata_settings_theme_json": "dark", + "metadata_tags_0_json": "featured" + } + ] +} +``` + +```json [GraphQL] +{ + "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. + +::callout{icon="material-symbols:warning-rounded" color="warning"} + +**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. + +:: + +::code-group + +```http [REST] +GET /items/articles?fields=id,json(metadata, dimensions),json(metadata, tags) +``` + +```graphql [GraphQL] +query { + articles { + id + metadata_func { + dimensions: json(path: "dimensions") + tags: json(path: "tags") + } + } +} +``` + +```js [SDK] +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: + +::code-group + +```json [REST / SDK] +{ + "data": [ + { + "id": 1, + "metadata_dimensions_json": { "width": 100, "height": 50 }, + "metadata_tags_json": ["featured", "new"] + } + ] +} +``` + +```json [GraphQL] +{ + "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. + +::code-group + +```http [REST] +GET /items/articles?fields=id,title,category_id.name,json(category_id.metadata, color) +``` + +```graphql [GraphQL] +query { + articles { + id + title + category_id { + name + metadata_func { + color: json(path: "color") + } + } + } +} +``` + +```js [SDK] +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: + +::code-group + +```json [REST / SDK] +{ + "data": [ + { + "id": 1, + "title": "An Article", + "category_id": { + "name": "News", + "metadata_color_json": "blue" + } + } + ] +} +``` + +```json [GraphQL] +{ + "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. + +::code-group + +```http [REST] +GET /items/articles/1?fields=id,json(comments.data, type) +``` + +```graphql [GraphQL] +query { + articles_by_id(id: 1) { + id + comments { + data_func { + json(path: "type") + } + } + } +} +``` + +```js [SDK] +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: + +::code-group + +```json [REST / SDK] +{ + "data": { + "id": 1, + "comments": [ + { "data_type_json": "comment" }, + { "data_type_json": "review" } + ] + } +} +``` + +```json [GraphQL] +{ + "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. + +::code-group + +```http [REST] +GET /items/shapes/1?fields=id,json(children.item:circles.metadata, color) +``` + +```graphql [GraphQL] +query { + shapes_by_id(id: 1) { + id + children { + item { + ... on circles { + metadata_func { + json(path: "color") + } + } + } + } + } +} +``` + +```js [SDK] +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: + +::code-group + +```json [REST / SDK] +{ + "data": { + "id": 1, + "children": [ + { + "item": { + "metadata_color_json": "red" + } + } + ] + } +} +``` + +```json [GraphQL] +{ + "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`, 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. + +::callout{icon="material-symbols:warning-rounded" color="warning"} +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. + +```typescript +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("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](#response-format). For a relational field, the extracted alias appears typed on the related item (e.g. `items[0].category_id.metadata_color_json`). + +::callout{icon="material-symbols:info-outline"} +**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. + +::callout{icon="material-symbols:warning-rounded" color="warning"} +`_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](#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"`. + +::code-group + +```http [REST] +GET /items/articles + ?filter={"metadata":{"_json":{"color":{"_eq":"blue"}}}} +``` + +```graphql [GraphQL] +query { + articles(filter: { metadata: { _json: { color: { _eq: "blue" } } } }) { + id + title + } +} +``` + +```js [SDK] +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: + +```json +{ + "data": [ + { "id": 1, "title": "An Article" }, + { "id": 4, "title": "Another Article" } + ] +} +``` + +### Multiple Path Conditions + +Combine several path conditions inside a single `_json` object. + +::code-group + +```http [REST] +GET /items/articles + ?filter={"metadata":{"_json":{"color":{"_eq":"red"},"brand":{"_in":["BrandX","BrandY"]},"level":{"_gte":3}}}} +``` + +```graphql [GraphQL] +query { + articles( + filter: { + metadata: { + _json: { + color: { _eq: "red" } + brand: { _in: ["BrandX", "BrandY"] } + level: { _gte: 3 } + } + } + } + ) { + id + title + } +} +``` + +```js [SDK] +const result = await directus.request( + readItems("articles", { + filter: { + metadata: { + _json: { + color: { _eq: "red" }, + brand: { _in: ["BrandX", "BrandY"] }, + level: { _gte: 3 }, + }, + }, + }, + }), +); +``` + +:: + +Response: + +```json +{ + "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. + +::code-group + +```http [REST] +GET /items/articles + ?filter={"metadata":{"_json":{"settings.theme":{"_eq":"dark"},"tags[0]":{"_eq":"electronics"}}}} +``` + +```graphql [GraphQL] +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 } +# } +# } +``` + +```js [SDK] +const result = await directus.request( + readItems("articles", { + filter: { + metadata: { + _json: { + "settings.theme": { _eq: "dark" }, + "tags[0]": { _eq: "electronics" }, + }, + }, + }, + }), +); +``` + +:: + +Response: + +```json +{ + "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. + +::code-group + +```http [REST] +GET /items/articles + ?filter={"category_id":{"metadata":{"_json":{"color":{"_eq":"blue"}}}}} +``` + +```graphql [GraphQL] +query { + articles( + filter: { category_id: { metadata: { _json: { color: { _eq: "blue" } } } } } + ) { + id + title + category_id { + name + } + } +} +``` + +```js [SDK] +const result = await directus.request( + readItems("articles", { + filter: { + category_id: { + metadata: { + _json: { color: { _eq: "blue" } }, + }, + }, + }, + }), +); +``` + +:: + +Response: + +```json +{ + "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`. + +::code-group + +```http [REST] +GET /items/articles + ?filter={"_and":[{"metadata":{"_json":{"color":{"_eq":"blue"}}}},{"metadata":{"_json":{"size":{"_gt":10}}}}]} +``` + +```graphql [GraphQL] +query { + articles( + filter: { + _and: [ + { metadata: { _json: { color: { _eq: "blue" } } } } + { metadata: { _json: { size: { _gt: 10 } } } } + ] + } + ) { + id + title + } +} +``` + +```js [SDK] +const result = await directus.request( + readItems("articles", { + filter: { + _and: [ + { metadata: { _json: { color: { _eq: "blue" } } } }, + { metadata: { _json: { size: { _gt: 10 } } } }, + ], + }, + }), +); +``` + +:: + +Response: + +```json +{ + "data": [{ "id": 3, "title": "Large Blue Article" }] +} +``` + +Conditions can also be grouped within the `_json` operator using `_and` or `_or`: + +```json +{ + "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"`.