shacl2cypher
← Shapes for Property Graphs

4. Property shapes and cardinality

Is this property required? Can it have more than one value? Two constraints answer both questions. What needs care is what counts as a value.

Four patterns

04-cardinality.ttl:

ex:PersonShape a sh:NodeShape ;
    sh:targetClass ex:Person ;
    # Required and single-valued.
    sh:property [ sh:path ex:name ; sh:minCount 1 ; sh:maxCount 1 ] ;
    # Optional, but at most one value.
    sh:property [ sh:path ex:email ; sh:maxCount 1 ] ;
    # A list property: between one and three distinct skills.
    sh:property [ sh:path ex:skills ; sh:minCount 1 ; sh:maxCount 3 ] ;
    # A relationship: at most one employer.
    sh:property [ sh:path ex:worksFor ; sh:class ex:Company ; sh:maxCount 1 ] .
You meanWrite
requiredsh:minCount 1
optional, single-valuedsh:maxCount 1
exactly onesh:minCount 1 ; sh:maxCount 1
between n and msh:minCount n ; sh:maxCount m
optional, any numbernothing: that is the default

SHACL has no "required" keyword. Absence is just "fewer than one value".

What counts as a value

Every path gives each focus node a set of values. In a property graph that set is built like this:

  • A scalar property is one value: name: "Ann" → {"Ann"}.
  • A list property is one value per element: skills: ["go", "sql"] → {"go", "sql"}.
  • A property that is absent, null, or an empty list is the empty set.
  • null elements inside a list are dropped.
  • Counts are over distinct values: ["a", "a"] is one value.
  • For a relationship path, the values are the distinct nodes reached.

shacl2cypher's conformance suite pins this behaviour down with a fixture that runs against pySHACL, Neo4j and LadybugDB. For shapes name (min 1), tags (min 1, max 2) and codes (max 1):

Node dataViolations
{name: "Ann", tags: ["a","b"], codes: ["x"]}none
{}name minCount, tags minCount
{name: null, tags: []}name minCount, tags minCount
{name: "Bo", tags: ["a","b","c"]}tags maxCount
{name: "Cy", tags: ["a","a"], codes: ["x","x"]}none: duplicates count once
{name: "Di", tags: ["a"], codes: ["x","y"]}codes maxCount

Why sh:maxCount 1 matters on Neo4j

Neo4j allows any property to hold a list, and nothing stops an import script from writing email: ["a@x", "b@x"] where the application expects a string. sh:maxCount 1 catches it. On LadybugDB a STRING column can't hold a list, but the rule still documents intent and still guards relationships.

Treating a list as one value

If a property is a list as a whole, say coordinates [lat, lon], you may want sh:maxCount 1 to mean "one list". Annotate it:

ex:location s2c:collection "scalar" .

What a cardinality violation looks like

Counting constraints report once per focus node, not per value, so the row's value is null:

failed      Violation           1         2  PersonShape.email.minCount
            - Person id=p2: social:PersonShape violates sh:minCount on social:email

That line comes from the LPG Modeler walkthrough, where a model's required: true becomes exactly this constraint.

Takeaways

  • sh:minCount and sh:maxCount express required, optional, single- and multi-valued.
  • Absent, null and [] are all zero values; list elements count separately; duplicates count once.
  • Only counting constraints and sh:hasValue notice that a value is missing. Everything in the next chapter ignores empty sets.