Skip to main content

SHACL Validation

SHACL (Shapes Constraint Language) is a W3C standard for validating RDF graphs. Fluree supports SHACL shapes to enforce data integrity constraints on your ledger.


Overview

SHACL shapes define constraints that data must conform to when inserted or updated. When a transaction violates a SHACL constraint, Fluree rejects the transaction with a detailed validation report.

Key concepts:

  • Shapes define constraints on data
  • Target classes specify which entities a shape applies to
  • Property constraints validate specific properties
  • Validation reports explain what went wrong

Defining Shapes

Shapes are defined by inserting entities of type sh:NodeShape:


{
"@context": {
"sh": "http://www.w3.org/ns/shacl#",
"schema": "http://schema.org/",
"ex": "http://example.org/",
"xsd": "http://www.w3.org/2001/XMLSchema#"
},
"insert": {
"@id": "ex:UserShape",
"@type": "sh:NodeShape",
"sh:targetClass": {"@id": "ex:User"},
"sh:property": [
{
"sh:path": {"@id": "schema:name"},
"sh:minCount": 1,
"sh:maxCount": 1,
"sh:datatype": {"@id": "xsd:string"}
}
]
}
}

Once defined, the shape applies to all entities of the target class.

Property Constraints

Cardinality

Control how many values a property can have:

ConstraintDescriptionExample
sh:minCountMinimum number of values"sh:minCount": 1 (required)
sh:maxCountMaximum number of values"sh:maxCount": 1 (single value)

{
"sh:property": [{
"sh:path": {"@id": "schema:email"},
"sh:minCount": 1,
"sh:maxCount": 1
}]
}

Datatype

Require values to be a specific datatype:


{
"sh:property": [{
"sh:path": {"@id": "schema:age"},
"sh:datatype": {"@id": "xsd:integer"}
}]
}

Common datatypes:

  • xsd:string — Text values
  • xsd:integer — Whole numbers
  • xsd:decimal — Decimal numbers
  • xsd:boolean — True/false
  • xsd:date — Date values
  • xsd:dateTime — Date and time values

Numeric Ranges

Constrain numeric values to a range:

ConstraintDescription
sh:minInclusiveMinimum value (inclusive)
sh:maxInclusiveMaximum value (inclusive)
sh:minExclusiveMinimum value (exclusive)
sh:maxExclusiveMaximum value (exclusive)

{
"sh:property": [{
"sh:path": {"@id": "schema:age"},
"sh:minInclusive": 0,
"sh:maxInclusive": 150
}]
}

String Constraints

Validate string properties:

ConstraintDescription
sh:minLengthMinimum string length
sh:maxLengthMaximum string length
sh:patternRegex pattern to match

{
"sh:property": [{
"sh:path": {"@id": "schema:email"},
"sh:pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
}]
}

Enumerated Values

Restrict values to a specific set using sh:in:


{
"sh:property": [{
"sh:path": {"@id": "ex:status"},
"sh:in": ["active", "inactive", "pending"]
}]
}

Class Membership

Require referenced entities to be of a specific class:


{
"sh:property": [{
"sh:path": {"@id": "schema:author"},
"sh:class": {"@id": "schema:Person"}
}]
}

Node Kind

Specify whether values should be IRIs or literals:


{
"sh:property": [{
"sh:path": {"@id": "schema:knows"},
"sh:nodeKind": {"@id": "sh:IRI"}
}]
}

Node kinds:

  • sh:IRI — Must be a reference (IRI)
  • sh:Literal — Must be a literal value
  • sh:BlankNode — Must be a blank node

Property Pairs

Compare values between properties:

sh:equals

Require two properties to have the same values:


{
"sh:property": [{
"sh:path": {"@id": "ex:displayName"},
"sh:equals": {"@id": "schema:name"}
}]
}

sh:disjoint

Require two properties to have no common values:


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

Closed Shapes

Prevent entities from having properties not declared in the shape:


{
"@id": "ex:StrictUserShape",
"@type": "sh:NodeShape",
"sh:targetClass": {"@id": "ex:User"},
"sh:closed": true,
"sh:ignoredProperties": [{"@id": "rdf:type"}],
"sh:property": [
{"sh:path": {"@id": "schema:name"}},
{"sh:path": {"@id": "schema:email"}}
]
}

With sh:closed: true, any property not listed in sh:property or sh:ignoredProperties will cause a validation error.

NOTE: The rdf:type property (or @type in JSON-LD) should typically be in sh:ignoredProperties since it's used to trigger the shape itself.

Logical Constraints

Combine constraints using logical operators:

sh:not

Ensure data does NOT conform to a shape:


{
"@id": "ex:NonEmployeeShape",
"@type": "sh:NodeShape",
"sh:targetClass": {"@id": "ex:Person"},
"sh:not": [{
"sh:path": {"@id": "ex:employeeId"},
"sh:minCount": 1
}]
}

This shape ensures persons do NOT have an employee ID.

sh:or

Require at least one of several constraints to be satisfied:


{
"sh:property": [{
"sh:path": {"@id": "ex:contact"},
"sh:or": [
{"sh:datatype": {"@id": "xsd:string"}},
{"sh:nodeKind": {"@id": "sh:IRI"}}
]
}]
}

sh:and

Require all constraints to be satisfied (useful for combining):


{
"sh:property": [{
"sh:path": {"@id": "schema:age"},
"sh:and": [
{"sh:minInclusive": 18},
{"sh:maxInclusive": 65}
]
}]
}

Validation Reports

When a transaction violates SHACL constraints, Fluree returns an error with a validation report:


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

Report Fields

FieldDescription
sh:focusNodeThe entity that failed validation
sh:constraintComponentThe constraint that was violated
sh:sourceShapeThe shape that defined the constraint
sh:resultPathThe property path that failed
sh:valueThe actual value found
f:expectationThe expected value
sh:resultMessageHuman-readable error message

Complete Example

Here's a comprehensive example with multiple constraints:


{
"@context": {
"sh": "http://www.w3.org/ns/shacl#",
"schema": "http://schema.org/",
"ex": "http://example.org/",
"xsd": "http://www.w3.org/2001/XMLSchema#"
},
"insert": {
"@id": "ex:PersonShape",
"@type": "sh:NodeShape",
"sh:targetClass": {"@id": "ex:Person"},
"sh:property": [
{
"@id": "ex:nameConstraint",
"sh:path": {"@id": "schema:name"},
"sh:minCount": 1,
"sh:maxCount": 1,
"sh:datatype": {"@id": "xsd:string"},
"sh:minLength": 2,
"sh:maxLength": 100
},
{
"@id": "ex:emailConstraint",
"sh:path": {"@id": "schema:email"},
"sh:maxCount": 3,
"sh:pattern": "^[^@]+@[^@]+\\.[^@]+$"
},
{
"@id": "ex:ageConstraint",
"sh:path": {"@id": "schema:age"},
"sh:datatype": {"@id": "xsd:integer"},
"sh:minInclusive": 0,
"sh:maxInclusive": 150
},
{
"@id": "ex:statusConstraint",
"sh:path": {"@id": "ex:status"},
"sh:in": ["active", "inactive", "pending"]
}
]
}
}

This shape ensures:

  • Name is required, single-valued, string between 2-100 characters
  • Email is optional but max 3 values, must match email pattern
  • Age is optional but must be integer 0-150
  • Status must be one of the allowed values

Best Practices

  1. Give shapes and property constraints IDs — Makes validation reports more readable
  2. Use sh:message — Add custom error messages for better debugging
  3. Start permissive, add constraints gradually — Easier to tighten than loosen
  4. Test constraints before deploying — Ensure they don't block valid data
  5. Consider sh:severity — Use sh:Warning for non-blocking validation