Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

JSON documents and relationships

MATCH (person:Person)-[link:KNOWS]->(friend:Person)
RETURN person.name, friend.name, link.since

A document’s array can supply the rows behind a relationship. Map the parent ID, extract each array element’s target ID and properties, then traverse those edges with ordinary Cypher or Gremlin. JSON functions also work directly in expressions:

RETURN json.value(JSON '{"author":{"name":"Ada"}}', '$.author.name') AS author,
       json.value(JSON '{"count":7}', '$.count', 'int64') AS count

JSON values have their own type. Use a JSON '...' literal or json.parse(text); ordinary strings remain strings. Register a document column with schema type json in a compiler request, or use ir::functions::domain::json_type() for an Arrow schema in Rust.

Turn an array into edges

Suppose each person stores a document like this in profile:

{"friends":[{"id":2,"since":2020},{"id":3,"since":2023}]}

Use jsonb for the PostgreSQL column or JSON for the DuckDB column:

-- PostgreSQL
CREATE TABLE people (id BIGINT PRIMARY KEY, name TEXT, profile JSONB);
-- DuckDB
CREATE TABLE people (id BIGINT PRIMARY KEY, name VARCHAR, profile JSON);

Both tables can hold these example rows:

INSERT INTO people VALUES
  (1, 'Ada', '{"friends":[{"id":2,"since":2020},{"id":3,"since":2023}]}'),
  (2, 'Grace', '{"friends":[]}'),
  (3, 'Katherine', '{"friends":[]}');

The mapping below expands profile.friends into a named relation. Its fields become the source ID, target ID, and relationship properties:

[node.Person]
table = "people"
id = "id"

[node.Person.properties]
name = "name"
profile = "profile"

[collection_sources]
catalog = '''[
  {
    "name": "friend_links",
    "table": "people",
    "expand": "json.elements(profile, '$.friends')",
    "as": "item",
    "parent_columns": {"source_id": "id"},
    "ordinality": "position",
    "fields": {
      "target_id": "json.value(item.value, '$.id', 'int64')",
      "since": "json.value(item.value, '$.since', 'int64')"
    }
  }
]'''

[edge.KNOWS]
table = "friend_links"
src = "source_id"
dst = "target_id"
src_label = "Person"
dst_label = "Person"
edge_id = ["source_id", "position"]

[edge.KNOWS.properties]
since = "since"

Load the mapping with GraphMapping::from_toml and register the people schema or provider, as in the mapping reference. For the JSON compiler, put the same collection descriptor in the collection_sources array and use data_type: "json" for profile.

The opening query now returns Ada’s two friendships. Repeated array elements remain separate occurrences. position starts at one and distinguishes those edges; use an embedded stable relationship ID instead if identity must survive array reordering. json.elements itself exposes a zero-based index field.

Missing paths, SQL-null documents, and empty arrays produce no rows. A present value of the wrong kind is an error: json.elements requires an array. Set "outer": true on a collection source to retain parents with no child rows; the generated child fields and ordinality are null. Null endpoints do not form ordinary graph edges. A collection can use another collection as its source to expand nested documents.

Read and construct documents

MATCH (p:Person)
WHERE json.exists(p.profile, '$.friends')
RETURN p.name,
       json.array_length(p.profile, '$.friends') AS friend_count,
       json.query(p.profile, '$.friends') AS friends

json.query returns JSON. json.value returns a scalar string by default; pass a literal type name such as int64, float64, or boolean for a typed result. Missing paths return SQL null. An existing JSON null is preserved by json.query, while json.value returns SQL null for JSON null, objects, and arrays.

Construct values without string concatenation:

RETURN json.object('name', 'Ada', 'tags', json.array('graph', 'sql')) AS document

Strings stay JSON strings; JSON arguments embed their document value. SQL-null constructor values become JSON null. Object keys must be non-null strings. Duplicate object keys keep the last value.

json.transform converts a document to an explicitly typed nested value:

RETURN json.transform(
  JSON '{"id":7,"tags":["graph","sql"]}',
  JSON '{"id":"int64","tags":["string"]}'
) AS record

A schema object declares fields, a one-element schema array declares a list, and a type-name string declares a scalar. Missing fields become typed nulls. Invalid type conversions produce an error. The schema can also be supplied as a constant string.

Use documents in relationship rules

For example, map a document node’s payload and a requirement node’s criteria as JSON properties, then define containment:

[edge.MATCHES]
source = "Document"
target = "Requirement"
predicate = "json.contains(source.payload, target.criteria)"
MATCH (document:Document)-[:MATCHES]->(requirement:Requirement)
RETURN document.id, requirement.id

Containment checks the requested structure. Objects match a subset of their fields; arrays match an unordered subset, ignoring repeated requested values. {"a":{"b":1}} does not contain {"b":1} at the root. Use json.query first when containment should be tested within a particular subtree.

Function catalog

FunctionsResult
json.parse(text), json.stringify(document)Parse text as JSON; serialize JSON as text.
json.valid(text)Whether text is valid JSON.
json.query(document, path)JSON at a path; multiple-selection paths return a JSON array.
json.value(document, path[, type])Typed scalar; default type is string.
json.exists(document, path)Whether the path exists, including JSON-null values.
json.type(document[, path])null, boolean, number, string, array, or object.
json.keys(document[, path])Object keys as a list of strings.
json.array_length(document[, path])Array length.
json.elements(document[, path])Array elements as rows.
json.entries(document[, path])Object members as rows.
json.tree(document[, path])Selected value and its descendants as rows.
json.array(values...), json.object(key, value, ...)Construct JSON.
json.transform(document, schema)Typed fields and lists.
json.set(document, path, value)Set an existing value or add a final member.
json.insert(document, path, value)Add a missing object member or insert before an array position.
json.replace(document, path, value)Replace only an existing value.
json.remove(document, paths...)Remove selected members or array elements.
json.merge_patch(document, patch)Apply a JSON merge patch; null patch fields remove object members.
json.equals(left, right), json.contains(document, candidate)Structural comparison.
json.array_agg(value), json.object_agg(key, value)Aggregate rows into JSON.

The row functions return lists of records when used as scalar expressions and can supply collection-source rows. Each record contains value, index, key, path, parent_path, and depth. Aggregates retain null inputs as JSON null; empty input returns SQL null. Specify ordering when array order or the last value of a repeated object key matters.

Execution and supported paths

OrchidDB has native implementations of these functions. PostgreSQL and DuckDB lower supported operations into their own JSON expressions and row-producing operations, so document work can compose with surrounding joins and filters. Both adapters support document reads, constructors, equality, containment, merge patch, aggregates, and collection expansion. DuckDB also lowers nested json.transform results and text validation. PostgreSQL lowers path mutations and scalar/list transforms; transforms containing structs execute natively. DuckDB path mutations currently execute natively. SQL generation reports unsupported combinations explicitly; backend errors are not retried with different semantics.

Use definite paths such as $.friends[0].id, $["key.with.dots"], or JSON Pointer /friends/0/id for portable SQL examples. The native path evaluator also supports wildcards, recursive descent, unions, slices, and basic filters. Array positions are zero based; negative JSONPath positions count backward. Mutation functions require definite paths. See the detailed JSON reference for path grammar, null behavior, mutation rules, and engine capability limits.