diff --git a/content/_partials/json-function.md b/content/_partials/json-function.md new file mode 100644 index 00000000..37e2bb78 --- /dev/null +++ b/content/_partials/json-function.md @@ -0,0 +1,215 @@ +## 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 85cd4319..7ec38a64 100644 --- a/content/_partials/query-functions.md +++ b/content/_partials/query-functions.md @@ -13,3 +13,4 @@ The syntax for using a function is `function(field)`. | `minute` | Extract the minute from a datetime/date/timestamp field | | `second` | Extract the second from a datetime/date/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/configuration/security-limits.md b/content/configuration/security-limits.md index f523eaa8..186c5a7c 100644 --- a/content/configuration/security-limits.md +++ b/content/configuration/security-limits.md @@ -150,6 +150,7 @@ Allows you to configure hard technical limits, to prevent abuse and optimize for | `MAX_PAYLOAD_SIZE` | Controls the maximum request body size. Accepts number of bytes, or human readable string. | `1mb` | | `MAX_BATCH_MUTATION` | The maximum number of items for batch mutations when creating, updating and deleting. | `Infinity` | | `MAX_RELATIONAL_DEPTH` | The maximum depth when filtering / querying relational fields, with a minimum value of `2`. | `10` | +| `MAX_JSON_QUERY_DEPTH` | The maximum json path depth when querying JSON fields. | `10` | | `QUERY_LIMIT_DEFAULT` | The default query limit used when not defined in the API request. | `100` | | `QUERY_LIMIT_MAX` | The maximum query limit accepted on API requests. | `-1` | | `QUERYSTRING_MAX_PARSE_DEPTH` | The maximum object depth when parsing URL query parameters using the querystring format | `10` | diff --git a/content/guides/04.connect/3.query-parameters.md b/content/guides/04.connect/3.query-parameters.md index 59e33434..bc18b546 100644 --- a/content/guides/04.connect/3.query-parameters.md +++ b/content/guides/04.connect/3.query-parameters.md @@ -40,7 +40,7 @@ Use native GraphQL queries. :: ::callout{icon="material-symbols:info-outline"} -**Wildcards and performance** +**Wildcards and performance** While wildcards are very useful, we recommend only requesting specific fields in production. By only requesting the fields you need, you can speed up the request, and reduce the overall output size. :: @@ -624,6 +624,8 @@ const result = await directus.request( ``` :: +:partial{content="json-function"} + ## 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.