Skip to content

@PropertyBag Annotation

Maps an open Map<String, *> field to a set of flat, prefixed node properties — round-tripped on save and load. Unlike @JsonPacked (one opaque JSON string), each entry becomes a real property, so it’s visible, filterable, and indexable. Mirrors Spring Data Neo4j’s @CompositeProperty (available as an alias).

@NodeFragment(labels = ["Proposition"])
data class PropositionNode(
@NodeId val id: String,
val text: String,
@PropertyBag val metadata: Map<String, Any?> = emptyMap(), // -> metadata.<key> properties
)

metadata = {"source": "wiki", "score": 3} persists as metadata.source = "wiki", metadata.score = 3 alongside id/text. Use prefix to decouple the graph namespace from the field name and delimiter to change the separator; a fragment may carry several bags.

  • Values must be storable Neo4j primitives or homogeneous arrays (String, Number, Boolean, temporal, or arrays/lists thereof) — a nested map/object throws an IllegalArgumentException naming the key.
  • Stale keys (removing an entry then saving) are removed under NullPolicy.CLEAR: the keys the node holds are read, and those the map no longer has are removed. The default IGNORE is a merge-patch and leaves them.
  • Read asymmetry: Map<String, Any?> reads back driver-mapped types (an Int written returns as Long).
  • Filter by key in the type-safe DSL — composes on the load path and inside loadNearest / loadMatching:
loadAll<PropositionView> { where { proposition.metadata.key("source") eq "wiki" } }
// -> WHERE proposition.`metadata.source` = $p

For a runtime key (not known at compile time), use the dynamic property(path) / field(key) predicates instead of .key(...).

Works across Neo4j, FalkorDB, and Memgraph.

A flat bag has no prefix: each entry is stored under its bare key, and the bag reads back every property that no declared field and no other bag accounts for. It fits a node whose properties are open-ended and were never namespaced. A fragment may have one, and an entry whose key is a declared field’s property is rejected at save.

@NodeFragment(labels = ["Thing"])
data class ThingNode(
@NodeId val id: String,
val name: String,
@PropertyBag(flat = true) val properties: Map<String, Any?> = emptyMap(), // -> <key> properties
)