Chapter 02 · Property Graph Model: Nodes, Relationships, Labels, Properties, Types, and Schema Discipline
Labels, Relationship Types, Properties, Lists, Maps, Temporal/Spatial Values, Null, and Missing Properties
Separate graph classification from value storage and make property/null behavior observable before filters, indexes, and schema contracts depend on it.
Learning outcomes
AtlasMart now has a graph shape, but production bugs often begin inside properties: mixing strings and dates, treating a query map as if it can be persisted whole, using heterogeneous lists where storage requires homogeneous property values, or assuming a missing property is a stored SQL-style NULL. This lesson makes the value model observable before later chapters depend on filters, indexes, or schema enforcement.
Differentiate labels, relationship types, properties, structural values, and constructed values.
Choose Neo4j property types for strings, numbers, booleans, temporal values, spatial points, and homogeneous stored lists.
Explain why maps are useful query/parameter values but cannot be stored directly as node or relationship properties.
Predict Cypher null/missing-property behavior, including IS NULL / IS NOT NULL and three-valued logic.
Detect inconsistent property shape before adding stronger indexes or constraints.
Continue Chapter 01's free/local baseline: Neo4j Community
2026.07.1, database neo4j, explicit
CYPHER 25 in version-sensitive examples, local
container atlasmart-neo4j, Bolt
127.0.0.1:7687, HTTP 127.0.0.1:7474,
and disposable password atlasmart-course-2026.
Existing AtlasMart identifiers use Community-supported
uniqueness constraints. This chapter adds
:Category, :Supplier,
:Store, and :Shipment identities and
makes graph-model contracts observable.
The generation environment does not provide a running Docker daemon or Neo4j server, so commands are documentation-checked but not presented as captured output. Community supports the uniqueness constraints used in the mandatory lab. Current property-existence, property-type, and key constraints are Enterprise-only; when those stronger contracts are discussed, the Community path uses validation queries plus application/import checks rather than fabricating successful Community DDL.
Classification and values are different modeling surfaces
A label such as :Product classifies a node; a
relationship type such as :IN_CATEGORY classifies
an edge. Properties hold values. They are not interchangeable. A
product with labels :Product:Discontinued uses a
second label as classification, while
status:'DISCONTINUED' uses a property that can
carry a broader value domain. The right choice depends on query
patterns, indexing, integrity, frequency of change, and whether
the concept is an entity, class, scalar, or connection.
Current Cypher property types include boolean, numeric, string, temporal, duration, point, list, and vector-related values. Nodes, relationships, and paths are structural values returned by pattern matching and are not themselves storable as properties. Maps and general lists are constructed query values; maps cannot be stored as properties, and stored lists must satisfy the property-list restrictions such as homogeneous element types and no null elements.
| Value/model surface | Can be stored as property? | AtlasMart example |
|---|---|---|
| String / integer / float / boolean | Yes |
Product.name, Order.total,
Supplier.active
|
| Temporal value | Yes |
Order.orderedAt,
Shipment.dispatchedAt
|
| Spatial point | Yes | Store.location |
| Homogeneous list of supported simple values | Yes | Product.tags=['outdoor','camera'] |
| Map | No, as a single stored property |
Useful as $product parameter and then copied
into individual properties
|
| Node / relationship / path | No | Returned/bound by pattern matching, not embedded as another property |
Build typed AtlasMart values explicitly
CYPHER 25MATCH (p:Product {productId:'P-1001'})SET p.tags=['outdoor','camera'], p.active=true, p.weightKg=0.42;MATCH (st:Store {storeId:'ST-TEH-01'})SET st.location=point({latitude:35.6892, longitude:51.3890}), st.openedOn=date('2024-03-21');MATCH (sh:Shipment {shipmentId:'SH-7001'})SET sh.dispatchedAt=datetime('2026-09-09T09:15:00Z'), sh.estimatedTransit=duration('P2D');
Temporal and spatial values are not decorative strings. If
AtlasMart stores '2026-09-09' as a string, later
date arithmetic and type-aware operations differ from storing
date('2026-09-09'). The same principle applies to
geographic coordinates: two numeric properties can be valid, but
a POINT value enables Neo4j's spatial semantics and
point indexes where relevant.
CYPHER 25MATCH (p:Product {productId:'P-1001'}), (st:Store {storeId:'ST-TEH-01'}), (sh:Shipment {shipmentId:'SH-7001'})RETURN p.tags, valueType(p.tags) AS tagsType, p.weightKg, valueType(p.weightKg) AS weightType, st.location, valueType(st.location) AS locationType, sh.dispatchedAt, valueType(sh.dispatchedAt) AS dispatchedType;
valueType() gives runtime evidence for the actual
values. Later Enterprise-only property type constraints can make
some of these expectations declarative; in the Community lab,
validation queries and import/application contracts perform that
role.
Maps are query values, not nested stored documents
A common document-database habit is to pass a nested map and
assume Neo4j will store the map as one property. Cypher can
accept maps as parameters and return maps, but a map is a
constructed value, not a storable property value. Use a map to
set individual properties with SET n += $map when
every map value is a legal property value, or model nested
entities/relationships when the nested structure has identity or
traversal semantics.
// conceptual parameter value sent by a driver:$product = { productId: 'P-3001', name: 'Graph Label Printer', price: 249.0, tags: ['store','labeling']}CYPHER 25MERGE (p:Product {productId:$product.productId})SET p += $productRETURN p.productId, p.name, p.tags;
The map itself remains a parameter/query object. Its individual
legal values become properties. If the payload contains
supplier: {supplierId:'SUP-1001', name:'Northwind
Optics'}, do not flatten it blindly unless that is the intentional
ownership model. AtlasMart already treats Supplier as a node
because suppliers have identity, relationships, and independent
lifecycle.
Null means missing or unknown, not a stored null property
In Cypher, reading a property that is absent returns
null. Comparing with = null does not
produce true; it produces null. Use
IS NULL and IS NOT NULL. This matters
because a filter accepts rows only when its predicate evaluates
to true. A nullable boolean expression that evaluates to null is
therefore not the same as false in expression semantics even
though both fail a WHERE predicate.
CYPHER 25MATCH (p:Product {productId:'P-1001'}) REMOVE p.color;MATCH (p:Product {productId:'P-1001'})RETURN p.color AS missingColor, p.color IS NULL AS isMissing, p.color = null AS wrongEquality, coalesce(p.color,'UNSPECIFIED') AS displayColor;
Expected invariant: missingColor is null,
isMissing is true, wrongEquality is
null rather than true, and displayColor is the
fallback string. The absence of color is a modeling
state, not proof that a user explicitly chose “unknown.” If the
distinction between “not collected,” “not applicable,”
“redacted,” and “unknown” matters, represent it explicitly with
another property or a richer domain model.
Cypher can construct heterogeneous lists during query processing, but property storage is stricter: stored property lists must be homogeneous supported values and cannot contain null. Do not treat a node property as an arbitrary JSON array.
Deliberately break the property contract
AtlasMart decides that Product.price must be
numeric, but an import accidentally writes
'129.90' as a string on one product. Community does
not provide the Enterprise property-type constraint used later
in the course, so we first prove the drift with runtime types.
CYPHER 25MERGE (p:Product {productId:'P-BAD-PRICE'})SET p.name='Bad Import Example', p.price='129.90';CYPHER 25MATCH (p:Product)WHERE p.price IS NOT NULL AND NOT (p.price IS :: INTEGER | FLOAT)RETURN p.productId, p.price, valueType(p.price) AS actualType;
The repair is not “cast every read forever.” Fix the ingest contract and correct existing data under controlled rules:
CYPHER 25MATCH (p:Product {productId:'P-BAD-PRICE'})SET p.price=toFloat(p.price);MATCH (p:Product {productId:'P-BAD-PRICE'})RETURN p.productId, p.price, valueType(p.price) AS actualType;
In Enterprise, a property-type constraint can turn this expectation into a database-enforced contract. In Community, the course keeps the learning objective reproducible with validation queries, tests, and deterministic import checks.
Lab: audit nulls, types, lists, and spatial/temporal values
CYPHER 25MATCH (n)UNWIND keys(n) AS propertyKeyRETURN labels(n) AS labels, propertyKey, count(*) AS occurrencesORDER BY labels, propertyKey;CYPHER 25MATCH (p:Product)RETURN p.productId, p.price, valueType(p.price) AS priceType, p.tags, valueType(p.tags) AS tagsTypeORDER BY p.productId;CYPHER 25MATCH (sh:Shipment)RETURN sh.shipmentId, sh.dispatchedAt, sh.estimatedTransitORDER BY sh.shipmentId;
Verification checklist: no stored list contains null; maps are not being persisted as nested document values; identifiers remain strings consistently; money/quantity semantics are documented; temporal values use the intended timezone/local type; point coordinate order and CRS are explicit; and every “missing” property has a business interpretation rather than accidental drift.
Check your understanding
- Why can a map be passed as a parameter but not stored directly as a node property?
- What does reading a missing property return in Cypher?
-
Why is
p.color = nullthe wrong missing-property test? - Can a stored property list contain mixed strings and integers or null elements?
-
Why might
date()be preferable to a date-looking string?
Review the answers
1. Maps are constructed query values; Neo4j properties accept property types, not arbitrary nested map values.
2. It returns null.
3. Null follows three-valued logic; equality to null evaluates to null. Use IS NULL/IS NOT NULL.
4. No. Property-list storage requires homogeneous supported values and does not allow null elements.
5. A temporal value carries date semantics for type-aware comparison/arithmetic and avoids relying on string conventions.
Summary and next step
Neo4j's property model is flexible but not JSON-with-arrows. Structural graph values, maps, stored properties, and property lists have distinct rules; null represents missing/unknown state and must be tested with Cypher's null semantics. Next, turn these observations into an explicit schema contract for naming, cardinality, required properties, and invariants.
Authoritative references
- Current Neo4j versions — Official current-release and 5.26 LTS patch snapshot.
- Cypher Manual — Current Cypher language reference.
- Property, structural, and constructed values — Current property/storage type rules, including lists and maps.
- Working with null — Current missing-property and three-valued null semantics.
- Create constraints — Constraint syntax, integrity semantics, backing indexes, and edition boundaries.
- Scalar functions: elementId() — Current elementId() guarantees and warning against durable application identity.
- MATCH — Directed and undirected relationship-pattern matching semantics.
- CREATE — Node and relationship creation semantics.
- Naming rules and recommendations — Current naming recommendations and case sensitivity.