Skip to content

Nested attribute values abort ingestion on Neo4j/FalkorDB/Neptune while Kuzu already serialises them #1885

Description

@xiaoyaner0201

Summary

EntityNode.save, EntityEdge.save and the two bulk write paths branch on GraphProvider.KUZU to json.dumps(attributes), but every other provider spreads each attribute into its own property. A property graph stores only primitives or arrays of primitives, so a single nested attribute value fails the entire episode:

neo4j.exceptions.CypherTypeError: {code: Neo.ClientError.Statement.TypeError}
Property values can only be of primitive types or arrays thereof.
Encountered: Map{title -> String("..."), description -> String("...")}

The correct handling already exists in the codebase — it is simply not applied to the other providers.

Two distinct causes produce this, and they need different remedies

I collected 83 dead-lettered episodes carrying this error from a production deployment and separated them by whether the payload contains a literal JSON Schema type marker (type -> String("string")):

Cause Count What happened
Legitimately nested extraction 40 The model was correct; the source data really is an object
Schema echo 43 The model returned the JSON Schema field definition instead of a value

A correct extraction that still fails the write:

Map{title -> String("compose.yml.bak.qworkers.*"),
    description -> String("Compose file backup used for rolling back the queue worker patch.")}

A schema echo:

Map{description -> Map{description -> String("Brief description of the object."),
                       title -> String("Description"),
                       type -> String("string")}}

This distinction matters. A guard that merely drops non-primitive values would silently discard the 40 legitimate extractions, which is worse than failing loudly. Whatever remedy is chosen, correctly extracted nested data should survive.

Retrying does not help

Because roughly half of these are genuine data rather than a transient model error, replaying the episode reproduces the same failure. In my replay runs these entries failed again with an identical error.

The node and edge paths are not symmetric

_extract_entity_attributes validates shape via entity_type(**merged), so a malformed node attribute raises ValidationError and is rejected before the write. The edge path in resolve_extracted_edge assigns resolved_edge.attributes = merged with no equivalent validation, so the value reaches the driver unchanged.

The cap layer does not catch it either: _check_value_against_cap inspects only str and list, so a dict falls through to return False, 'ok'.

Deterministic reproduction, no database required:

from pydantic import BaseModel
from graphiti_core.utils.maintenance.attribute_utils import apply_capped_attributes

class Document(BaseModel):
    title: str | None = None
    description: str | None = None

echoed_schema = {
    "title": {"description": "The title or identifier of the document",
              "title": "Title", "type": "string"},
}

merged, dropped = apply_capped_attributes(
    echoed_schema, Document, {}, merge_mode="replace",
)
print(dropped)                                           # set()  -> cap dropped nothing
print({k: type(v).__name__ for k, v in merged.items()})  # {'title': 'dict'}
Document(**merged)                                       # node path: ValidationError
# the edge path performs no equivalent validation, so this dict reaches the driver

Affected call sites

  • graphiti_core/nodes.pyEntityNode.save
  • graphiti_core/edges.pyEntityEdge.save
  • graphiti_core/utils/bulk_utils.py — node and edge bulk payload construction

All four share the same if provider == KUZU: json.dumps(...) else: spread shape.

Environment

  • graphiti-core 0.29.3; re-verified by source inspection on main (c64e45c) and v0.30.2
  • Neo4j 5 community
  • Custom entity_types / edge_types supplied through the MCP server

Relationship to existing work

I am aware of #440 and #1109 and am not trying to compete with either. #440 carries the identical error text and has been open since July 2025; #1109 takes the serialisation approach but has grown to 9 files including unrelated community-operations changes. I would be glad to see either merged instead.

I am filing this because I could not find the two-cause breakdown recorded anywhere, and it changes what a correct fix must do. Related: #1399, #1153, #705, #1011.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions