Skip to main content

SHACL Validation

SHACL (Shapes Constraint Language) lets you define and enforce data quality rules in Fluree. While classes and properties describe what your data could look like, SHACL shapes define what your data must look like—enforcing constraints similar to NOT NULL, CHECK, and UNIQUE in relational databases.

What SHACL Does​

SHACL provides a standard way to describe constraints on your data:

  • Required fields: A person must have a name
  • Data types: Age must be an integer
  • Value ranges: Age must be between 0 and 150
  • Patterns: Email must match a valid format
  • Relationships: A manager must be a Person
  • Cardinality: A person can have at most one SSN

When you transact data that violates a SHACL shape, Fluree rejects the transaction with a detailed validation report explaining what went wrong.

SHACL vs. Data Modeling​

It's important to understand the distinction:

ConceptPurposeEffect
rdfs:Class / rdf:PropertyDescribe data structureEnables inference and queries
rdfs:domain / rdfs:rangeLink properties to typesEnables inference, doesn't enforce
SHACL shapesDefine constraintsRejects invalid data

You can use SHACL independently from data modeling, but they work well together—your ontology describes the intended structure, SHACL enforces it.

Basic Shape Structure​

A SHACL shape has three key parts:

  1. Target: What nodes does this shape apply to?
  2. Property constraints: Rules for specific properties
  3. Node constraints: Rules for the node itself

Here's a simple example:


{
"@context": {
"sh": "http://www.w3.org/ns/shacl#",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"ex": "https://example.com/ns/"
},
"ledger": "my-app",
"insert": {
"@id": "ex:PersonShape",
"@type": "sh:NodeShape",
"sh:targetClass": { "@id": "ex:Person" },
"sh:property": [
{
"sh:path": { "@id": "ex:name" },
"sh:minCount": 1,
"sh:datatype": { "@id": "xsd:string" }
}
]
}
}

This shape says: "Every ex:Person must have at least one ex:name, and it must be a string."


Targeting​

Shapes need to know which nodes to validate. Fluree supports several targeting methods:

Target by Class​

Validate all instances of a class:


{
"@id": "ex:PersonShape",
"@type": "sh:NodeShape",
"sh:targetClass": { "@id": "ex:Person" }
}

Target Specific Nodes​

Validate specific individuals:


{
"@id": "ex:AdminShape",
"@type": "sh:NodeShape",
"sh:targetNode": [
{ "@id": "ex:admin1" },
{ "@id": "ex:admin2" }
]
}

Target by Property Usage​

Target subjects or objects of a property:


{
"@id": "ex:AuthorShape",
"@type": "sh:NodeShape",
"sh:targetSubjectsOf": { "@id": "ex:wrote" }
}


{
"@id": "ex:BookShape",
"@type": "sh:NodeShape",
"sh:targetObjectsOf": { "@id": "ex:wrote" }
}


Cardinality Constraints​

Control how many values a property can have:

sh:minCount​

Require at least N values (enforces "required" fields):


{
"sh:path": { "@id": "ex:email" },
"sh:minCount": 1
}

sh:maxCount​

Allow at most N values (enforces uniqueness when set to 1):


{
"sh:path": { "@id": "ex:ssn" },
"sh:maxCount": 1
}

Combined Example​

Require exactly one primary email:


{
"sh:path": { "@id": "ex:primaryEmail" },
"sh:minCount": 1,
"sh:maxCount": 1,
"sh:datatype": { "@id": "xsd:string" }
}


Datatype Constraints​

sh:datatype​

Require values to be a specific type:


{
"@id": "ex:PersonShape",
"@type": "sh:NodeShape",
"sh:targetClass": { "@id": "ex:Person" },
"sh:property": [
{
"sh:path": { "@id": "ex:name" },
"sh:datatype": { "@id": "xsd:string" }
},
{
"sh:path": { "@id": "ex:age" },
"sh:datatype": { "@id": "xsd:integer" }
},
{
"sh:path": { "@id": "ex:birthDate" },
"sh:datatype": { "@id": "xsd:date" }
},
{
"sh:path": { "@id": "ex:active" },
"sh:datatype": { "@id": "xsd:boolean" }
}
]
}

sh:class​

Require values to be instances of a class:


{
"sh:path": { "@id": "ex:employer" },
"sh:class": { "@id": "ex:Organization" }
}

sh:nodeKind​

Require a specific kind of RDF node:

ValueMeaning
sh:LiteralString, number, date, etc.
sh:IRIA reference to another node
sh:BlankNodeAn anonymous node
sh:IRIOrLiteralEither IRI or literal
sh:BlankNodeOrIRIEither blank node or IRI
sh:BlankNodeOrLiteralEither blank node or literal

{
"sh:path": { "@id": "ex:manager" },
"sh:nodeKind": { "@id": "sh:IRI" }
}


String Constraints​

sh:minLength and sh:maxLength​

Control string length:


{
"sh:path": { "@id": "ex:username" },
"sh:minLength": 3,
"sh:maxLength": 20
}

sh:pattern​

Match against a regular expression:


{
"sh:path": { "@id": "ex:email" },
"sh:pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
"sh:message": "Must be a valid email address"
}

sh:flags​

Add regex modifiers:

FlagMeaning
iCase-insensitive
mMultiline (^ and $ match line boundaries)
sDotall (. matches newlines)
xExtended (ignore whitespace)

{
"sh:path": { "@id": "ex:code" },
"sh:pattern": "^[A-Z]{3}[0-9]{4}$",
"sh:flags": "i"
}


Numeric Range Constraints​

Control numeric and date/time values:

Inclusive Ranges​


{
"sh:path": { "@id": "ex:age" },
"sh:minInclusive": 0,
"sh:maxInclusive": 150
}

Exclusive Ranges​


{
"sh:path": { "@id": "ex:score" },
"sh:minExclusive": 0,
"sh:maxExclusive": 100
}

Date/Time Ranges​

Works with temporal values too:


{
"sh:path": { "@id": "ex:startDate" },
"sh:minInclusive": "2020-01-01",
"sh:datatype": { "@id": "xsd:date" }
}


Value Set Constraints​

sh:in​

Restrict to a specific set of allowed values:


{
"sh:path": { "@id": "ex:status" },
"sh:in": ["pending", "approved", "rejected"]
}

Works with node references too:


{
"sh:path": { "@id": "ex:priority" },
"sh:in": [
{ "@id": "ex:Low" },
{ "@id": "ex:Medium" },
{ "@id": "ex:High" }
]
}

sh:hasValue​

Require a specific value to be present:


{
"sh:path": { "@id": "ex:type" },
"sh:hasValue": "customer"
}


Property Pair Constraints​

Compare values between properties:

sh:equals​

Values must be identical:


{
"sh:path": { "@id": "ex:password" },
"sh:equals": { "@id": "ex:confirmPassword" }
}

sh:disjoint​

Values must not overlap:


{
"sh:path": { "@id": "ex:primaryEmail" },
"sh:disjoint": { "@id": "ex:secondaryEmail" }
}

sh:lessThan and sh:lessThanOrEquals​

Compare numeric or date values:


{
"sh:path": { "@id": "ex:startDate" },
"sh:lessThan": { "@id": "ex:endDate" }
}


{
"sh:path": { "@id": "ex:minPrice" },
"sh:lessThanOrEquals": { "@id": "ex:maxPrice" }
}


Logical Constraints​

Combine multiple constraints with boolean logic:

sh:and​

All constraints must pass:


{
"@id": "ex:AdultShape",
"@type": "sh:NodeShape",
"sh:and": [
{
"sh:property": [{
"sh:path": { "@id": "ex:age" },
"sh:minInclusive": 18
}]
},
{
"sh:property": [{
"sh:path": { "@id": "ex:hasConsent" },
"sh:hasValue": true
}]
}
]
}

sh:or​

At least one constraint must pass:


{
"@id": "ex:ContactableShape",
"@type": "sh:NodeShape",
"sh:or": [
{
"sh:property": [{
"sh:path": { "@id": "ex:email" },
"sh:minCount": 1
}]
},
{
"sh:property": [{
"sh:path": { "@id": "ex:phone" },
"sh:minCount": 1
}]
}
]
}

sh:not​

Constraint must NOT pass:


{
"sh:not": {
"sh:property": [{
"sh:path": { "@id": "ex:deleted" },
"sh:hasValue": true
}]
}
}

sh:xone​

Exactly one constraint must pass (exclusive or):


{
"@id": "ex:NameShape",
"@type": "sh:NodeShape",
"sh:xone": [
{
"sh:property": [{
"sh:path": { "@id": "ex:fullName" },
"sh:minCount": 1
}]
},
{
"sh:and": [
{
"sh:property": [{
"sh:path": { "@id": "ex:firstName" },
"sh:minCount": 1
}]
},
{
"sh:property": [{
"sh:path": { "@id": "ex:lastName" },
"sh:minCount": 1
}]
}
]
}
]
}


Nested Shape Constraints​

sh:node​

Values must conform to another shape:


{
"@context": {
"sh": "http://www.w3.org/ns/shacl#",
"ex": "https://example.com/ns/"
},
"ledger": "my-app",
"insert": [
{
"@id": "ex:AddressShape",
"@type": "sh:NodeShape",
"sh:property": [
{ "sh:path": { "@id": "ex:street" }, "sh:minCount": 1 },
{ "sh:path": { "@id": "ex:city" }, "sh:minCount": 1 },
{ "sh:path": { "@id": "ex:postalCode" }, "sh:minCount": 1 }
]
},
{
"@id": "ex:PersonShape",
"@type": "sh:NodeShape",
"sh:targetClass": { "@id": "ex:Person" },
"sh:property": [
{
"sh:path": { "@id": "ex:address" },
"sh:node": { "@id": "ex:AddressShape" }
}
]
}
]
}

sh:qualifiedValueShape​

Apply cardinality to values matching a specific shape:


{
"sh:path": { "@id": "ex:contact" },
"sh:qualifiedValueShape": {
"sh:property": [{
"sh:path": { "@id": "ex:type" },
"sh:hasValue": "primary"
}]
},
"sh:qualifiedMinCount": 1,
"sh:qualifiedMaxCount": 1
}

This says: "There must be exactly one contact where type is 'primary'."


Closed Shapes​

sh:closed​

Reject any properties not explicitly allowed:


{
"@id": "ex:StrictPersonShape",
"@type": "sh:NodeShape",
"sh:targetClass": { "@id": "ex:Person" },
"sh:closed": true,
"sh:property": [
{ "sh:path": { "@id": "ex:name" } },
{ "sh:path": { "@id": "ex:email" } }
]
}

With this shape, a Person with an ex:phone property would be rejected.

sh:ignoredProperties​

Exclude certain properties from the closed check:


{
"@id": "ex:StrictPersonShape",
"@type": "sh:NodeShape",
"sh:closed": true,
"sh:ignoredProperties": [
{ "@id": "rdf:type" }
],
"sh:property": [
{ "sh:path": { "@id": "ex:name" } }
]
}


Property Paths​

SHACL supports property paths for more complex constraints:

Simple Path​


{ "sh:path": { "@id": "ex:name" } }

Inverse Path​

Follow the property in reverse:


{ "sh:path": { "sh:inversePath": { "@id": "ex:parent" } } }

This validates children of a node (nodes that have this node as their parent).

Alternative Path​

Match any of several properties:


{
"sh:path": {
"sh:alternativePath": [
{ "@id": "ex:email" },
{ "@id": "ex:phone" }
]
},
"sh:minCount": 1
}


Custom Messages​

sh:message​

Provide custom error messages:


{
"sh:path": { "@id": "ex:email" },
"sh:pattern": "^.+@.+\\..+$",
"sh:message": "Please provide a valid email address"
}


Validation Reports​

When a transaction violates a SHACL shape, Fluree returns a 422 status with a detailed validation report:


{
"error": "shacl/violation",
"report": {
"@type": "sh:ValidationReport",
"sh:conforms": false,
"sh:result": [
{
"@type": "sh:ValidationResult",
"sh:resultSeverity": "sh:Violation",
"sh:focusNode": "ex:john",
"sh:resultPath": ["ex:name"],
"sh:constraintComponent": "sh:minCount",
"sh:sourceShape": "ex:PersonShape",
"sh:resultMessage": "count 0 is less than minimum count of 1"
}
]
}
}

The report tells you:

  • focusNode: Which entity failed validation
  • resultPath: Which property had the issue
  • constraintComponent: Which constraint failed
  • resultMessage: What went wrong

Complete Example​

Here's a comprehensive example showing multiple constraint types:


{
"@context": {
"sh": "http://www.w3.org/ns/shacl#",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"ex": "https://example.com/ns/"
},
"ledger": "company-app",
"insert": {
"@id": "ex:EmployeeShape",
"@type": "sh:NodeShape",
"sh:targetClass": { "@id": "ex:Employee" },
"sh:property": [
{
"sh:path": { "@id": "ex:employeeId" },
"sh:minCount": 1,
"sh:maxCount": 1,
"sh:pattern": "^E-[0-9]{5}$",
"sh:message": "Employee ID must be in format E-XXXXX"
},
{
"sh:path": { "@id": "ex:name" },
"sh:minCount": 1,
"sh:datatype": { "@id": "xsd:string" },
"sh:minLength": 1,
"sh:maxLength": 100
},
{
"sh:path": { "@id": "ex:email" },
"sh:minCount": 1,
"sh:pattern": "^[a-zA-Z0-9._%+-]+@company\\.com$",
"sh:message": "Must be a company email address"
},
{
"sh:path": { "@id": "ex:department" },
"sh:minCount": 1,
"sh:class": { "@id": "ex:Department" }
},
{
"sh:path": { "@id": "ex:salary" },
"sh:datatype": { "@id": "xsd:decimal" },
"sh:minInclusive": 0
},
{
"sh:path": { "@id": "ex:startDate" },
"sh:minCount": 1,
"sh:datatype": { "@id": "xsd:date" },
"sh:lessThanOrEquals": { "@id": "ex:endDate" }
},
{
"sh:path": { "@id": "ex:status" },
"sh:in": ["active", "onLeave", "terminated"]
}
]
}
}

Now transactions that don't conform will be rejected:


{
"ledger": "company-app",
"insert": {
"@id": "ex:emp1",
"@type": "ex:Employee",
"ex:employeeId": "12345",
"ex:name": "John Doe"
}
}

This fails because:

  • employeeId doesn't match the pattern (should be E-12345)
  • email is missing
  • department is missing
  • startDate is missing

Best Practices​

Start Permissive, Get Stricter​

Begin with minimal constraints and add more as you understand your data requirements.

Use Meaningful Messages​

Always include sh:message for complex patterns to help users understand what's expected.

Separate Concerns​

Create focused shapes rather than one giant shape:


{
"@id": "ex:ContactInfoShape",
"@type": "sh:NodeShape",
"sh:property": [...]
}
{
"@id": "ex:EmploymentShape",
"@type": "sh:NodeShape",
"sh:property": [...]
}
{
"@id": "ex:EmployeeShape",
"@type": "sh:NodeShape",
"sh:targetClass": { "@id": "ex:Employee" },
"sh:node": [
{ "@id": "ex:ContactInfoShape" },
{ "@id": "ex:EmploymentShape" }
]
}

Document Your Shapes​

Use rdfs:label and rdfs:comment on shapes:


{
"@id": "ex:EmployeeShape",
"@type": "sh:NodeShape",
"rdfs:label": "Employee Shape",
"rdfs:comment": "Validates employee records for HR system"
}


Limitations​

Current limitations to be aware of:

  • Enforcing only: SHACL validation rejects invalid transactions. There's no non-enforcing "report only" mode currently.
  • Path expressions: Zero-or-more (*), one-or-more (+), and optional (?) paths are not yet supported.
  • SHACL-AF: Advanced features like SPARQL-based constraints are not supported.

Next Steps​

  • Review the complete Data Types reference for all supported datatypes
  • See how SHACL complements Reasoning for comprehensive data governance
  • Explore the Transaction Syntax for more on how transactions are processed