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.

Beginner → Intermediate110–130 minutesProperty types and null-semantics labNeo4j 2026.07.1 Community · Cypher 25Last reviewed: September 2026

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.

01

Differentiate labels, relationship types, properties, structural values, and constructed values.

02

Choose Neo4j property types for strings, numbers, booleans, temporal values, spatial points, and homogeneous stored lists.

03

Explain why maps are useful query/parameter values but cannot be stored directly as node or relationship properties.

04

Predict Cypher null/missing-property behavior, including IS NULL / IS NOT NULL and three-valued logic.

05

Detect inconsistent property shape before adding stronger indexes or constraints.

Chapter 02 continuity contract

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.

Execution and edition note

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 · temporal, spatial, scalar, and list properties
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 · inspect runtime types and values
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.

Cypher · parameter-map idea versus persisted properties
// 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 · make missing-property semantics visible
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.

Stored lists have stricter rules than general query lists.

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 · create and diagnose type drift
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 · repair known-safe drift and verify
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 · Chapter 02 property audit
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

  1. Why can a map be passed as a parameter but not stored directly as a node property?
  2. What does reading a missing property return in Cypher?
  3. Why is p.color = null the wrong missing-property test?
  4. Can a stored property list contain mixed strings and integers or null elements?
  5. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.