Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/jbarrasa/goingmeta/llms.txt

Use this file to discover all available pages before exploring further.

Season 3, Episode 6 of Going Meta returns to SHACL validation in Neo4j with a significantly more advanced treatment than the introductory Session 3 from Season 1. This episode covers the full SHACL constraint authoring workflow using two complementary Python libraries — graphexpectations for constraint authoring and shapes-validation for compilation and execution — and demonstrates how to define node shapes, property shapes, custom sh:targetQuery targets, and how to interpret the violation report.

Watch the Recording

Season 3, Episode 6 — March 2026

Session Code

Colab notebook: GM_S3_6_validations.ipynb

Libraries

pip install shapes-validation graphexpectations --index-url https://test.pypi.org/simple/
  • graphexpectations — a fluent Python DSL for authoring SHACL constraints without writing Turtle directly
  • shapes-validation — compiles SHACL shapes to Cypher and runs them against a live Neo4j database
This session extends the foundational SHACL content from Going Meta Season 1, Episode 3. If you are new to SHACL in Neo4j, watch that episode first for the basics of n10s-based constraint loading.

Authoring Constraints with graphexpectations

The graphexpectations library exposes an expectation DSL grouped into ge.Set objects, each targeting a node type or a custom query. Constraints are added as method calls:
import graphexpectations as ge

supplierExpectations = ge.Set(nodeType="Supplier")
supplierExpectations.expect_property_values_to_match_regex(
    property="country", regex="^[A-Za-z]+$", message="R001_INVALID_COUNTRY"
)
supplierExpectations.expect_number_of_outgoing_relationship_to_be_between(
    relationship="SUPPLIES", min=2, message="R002_LOW_PRODUCT_OFFERING"
)

productExpectations = ge.Set("Product")
productExpectations.expect_number_of_property_values_to_be_between(
    property="unitPrice", min=1, max=1, message="R003_SINGLE_PRICE"
)
productExpectations.expect_property_values_to_be_between(
    property="unitPrice", minInclusive=10, maxExclusive=500, message="R004_PRICE_LIMIT"
)

customerExpectations = ge.Set("Customer")
customerExpectations.expect_outgoing_relationship_to_connect_to_nodes_of_type(
    relationship="ORDERS", targetType="Order", message="R005_CUST_BAD_SCHEMA"
)
customerExpectations.expect_number_of_outgoing_relationship_to_be_between(
    relationship="ORDERS", min="1", message="R006_CUST_NO_ORDERS"
)

# Custom target: American-supplied products only
americanProducts = ge.Set(query=" (focus:Product)-[:supplied_by]->(:Supplier { country: 'USA' }) ")
americanProducts.expect_property_values_to_be_between(
    property="productID", minExclusive=10, message="R007_US_PROD_ID"
)
Bundle the sets into a suite and inspect the generated Turtle:
s = ge.Suite(desc="suite of expectations for my Neo4j Northwind KG")
s.add_expectations([supplierExpectations, productExpectations])
s.print_suite()   # or .serialise()

Raw SHACL Turtle

You can also write SHACL shapes directly in Turtle. The example below is equivalent to the expectations above and uses sh:targetClass, sh:targetQuery, sh:pattern, sh:minCount/sh:maxCount, sh:minInclusive/sh:maxExclusive, and sh:class:
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix n4j: <neo4j://graph.schema#> .

[] a sh:NodeShape ;
    sh:property
        [ sh:maxExclusive 500 ;
          sh:message "R004_PRICE_LIMIT" ;
          sh:minInclusive 5 ;
          sh:path n4j:unitPrice ],
        [ sh:maxCount 1 ;
          sh:message "R003_SINGLE_PRICE" ;
          sh:minCount 1 ;
          sh:path n4j:unitPrice ],
        [ sh:message "R002_LOW_PRODUCT_OFFERING" ;
          sh:minCount 1 ;
          sh:path [ sh:inversePath n4j:SUPPLIES ] ] ;
    sh:targetClass n4j:Product .

[] a sh:NodeShape ;
    sh:property
        [ sh:message "R006_CUST_NO_ORDERS" ;
          sh:minCount "1" ;
          sh:path n4j:PURCHASED ],
        [ sh:class n4j:Order ;
          sh:message "R005_CUST_BAD_SCHEMA" ;
          sh:path n4j:PURCHASED ] ;
    sh:targetClass n4j:Customer .

[] a sh:NodeShape ;
    sh:property
        [ sh:message "R007_US_PROD_ID" ;
          sh:minExclusive 10 ;
          sh:path n4j:productID ] ;
    sh:targetQuery " (focus:Product)<-[:SUPPLIES]-(:Supplier { country: 'USA' }) " .

[] a sh:NodeShape ;
    sh:property
        [ sh:message "R001_INVALID_COUNTRY" ;
          sh:path n4j:country ;
          sh:pattern "^[A-Za-z]+$" ] ;
    sh:targetClass n4j:Supplier .
The sh:targetQuery constraint (used for R007_US_PROD_ID) targets only the subset of Product nodes that are supplied by an American supplier — a custom focus node pattern that goes beyond simple sh:targetClass targeting.

Running Validation with shapes-validation

The ValidationClient loads the SHACL shapes (Turtle string or file), compiles them to Cypher internally, and runs them against the database. You can validate the entire graph or a specific set of nodes:
from neo4j import GraphDatabase
from shapes_validation import ValidationClient, GraphConfig

with GraphDatabase.driver(NEO4J_URL, auth=(NEO4J_USR, NEO4J_PWD)) as driver:
    driver.verify_connectivity()
    client = ValidationClient(
        driver,
        config=GraphConfig(handle_vocab_uris="IGNORE", graph_mode="LPG")
    )
    summaries = client.import_shacl(shapes_ttl)
    print("Loaded constraints:", len(summaries))

    # Validate a specific set of nodes by internal ID
    report = client.validate_nodeset([5, 6])
    print("Conforms:", report.conforms)

Interpreting the Violation Report

The report can be converted to a Pandas DataFrame for tabular inspection:
import pandas as pd

pd.set_option('display.max_rows', None)
pd.set_option('display.max_columns', None)
pd.set_option('display.width', 1000)

df = pd.DataFrame(report.to_rows())
display(df[['focusNode', 'nodeType', 'propertyShape', 'offendingValue',
            'resultPath', 'severity', 'message']])
Each row in the report corresponds to one constraint violation and includes:
ColumnDescription
focusNodeThe internal Neo4j node ID of the violating node
nodeTypeThe label(s) of the focus node
propertyShapeThe SHACL property shape that was violated
offendingValueThe actual property value that caused the violation
resultPathThe property path the constraint applies to
severitysh:Violation, sh:Warning, or sh:Info
messageThe human-readable message set on the shape (e.g. R001_INVALID_COUNTRY)

Advanced Patterns Covered in This Session

Inverse Path Constraints

Use sh:inversePath to constrain nodes based on incoming relationships — for example, requiring that every Product is supplied by at least one Supplier.

Custom Target Queries

sh:targetQuery selects an arbitrary subgraph as the focus set, enabling constraints that apply only to contextually relevant nodes rather than all nodes of a given type.

Class Constraints on Relationships

sh:class on a property shape verifies that the target node of a relationship has the expected label — catching schema violations where relationships connect incompatible node types.

Count Constraints

sh:minCount and sh:maxCount enforce cardinality — for example, that a Product has exactly one unitPrice value and that each Customer has placed at least one order.
Use the message codes on each shape (R001_INVALID_COUNTRY, R002_LOW_PRODUCT_OFFERING, etc.) as structured identifiers in your data quality pipeline. They make it easy to group, filter, and track violations over time in dashboards or CI reports.

Build docs developers (and LLMs) love