Cookbook: SHACL Validation
SHACL (Shapes Constraint Language) is a W3C standard for defining constraints on graph data. In Fluree, SHACL shapes are evaluated at transaction time — invalid data is rejected before it’s committed (or logged as a warning, depending on your config).
This guide covers:
- When SHACL runs — with and without a config graph
- Enabling SHACL via the config graph
- Defining shapes — node shapes, property shapes, targets
- Constraint patterns — cardinality, datatype, ranges, patterns, values, class, pair, logical
- Subclass reasoning for
sh:class - Predicate-target shapes —
sh:targetSubjectsOf/sh:targetObjectsOf - Per-graph enable/disable and warn vs reject modes
- Storing shapes in a named graph with
f:shapesSource - Validation reports —
fluree validateover a ledger or file - What isn’t enforced yet
When SHACL runs
Fluree decides whether to run SHACL validation on each transaction using this order:
- If a config graph exists with
f:shaclDefaults— follow the configured settings per graph (enable/disable, mode). - If no config graph section is present — fall back to the shapes-exist heuristic: if any SHACL shapes are present in the database (as regular RDF triples), validation runs in
Rejectmode. If no shapes are present, validation is skipped entirely (zero overhead).
This means you can start using SHACL without writing any config — just transact shapes and they’re enforced.
Bulk import is deliberately exempt. The bulk-import pipeline never runs
SHACL — it is a trusted, high-throughput load path. If your source data must
conform, validate it before importing — fluree validate source.ttl --shacl shapes.ttl produces a full report (see
Validation reports) — so the ledger
starts clean; transaction-time validation keeps it clean from there.
The shacl feature must be enabled at build time (it’s on by default for the server and CLI binaries). See Standards and feature flags.
Enabling SHACL via the config graph
Writing ledger config is done via transactions into the config graph, whose IRI is always urn:fluree:{ledger_id}#config. See Writing config data for the full pattern.
Minimal config: enable SHACL, shapes in the default graph
@prefix f: <https://ns.flur.ee/db#> .
GRAPH <urn:fluree:mydb:main#config> {
<urn:config:main> a f:LedgerConfig ;
f:shaclDefaults [
f:shaclEnabled true ;
f:validationMode f:ValidationReject
] .
}
Notes:
f:shaclEnableddefaults tofalsewhen af:shaclDefaultssection exists without it — make the enable decision explicit.f:validationModedefaults tof:ValidationReject. Usef:ValidationWarnto log violations without failing the transaction.- With no explicit
f:shapesSource, shapes are compiled from the default graph (f:defaultGraph, g_id=0). See Storing shapes in a named graph to load from elsewhere.
Defining shapes
Shapes are ordinary RDF — transact them like any other data. They can be written in Turtle, TriG, or JSON-LD.
Node shape with property constraints
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix schema: <http://schema.org/> .
@prefix ex: <http://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:PersonShape a sh:NodeShape ;
sh:targetClass schema:Person ;
sh:property [
sh:path schema:name ;
sh:datatype xsd:string ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:message "Every person must have exactly one name"
] ;
sh:property [
sh:path schema:email ;
sh:datatype xsd:string ;
sh:pattern "^[^@]+@[^@]+\\.[^@]+$" ;
sh:message "Email must be a valid email address"
] ;
sh:property [
sh:path ex:age ;
sh:datatype xsd:integer ;
sh:minInclusive 0 ;
sh:maxInclusive 200
] .
Target types
| Target | Effect |
|---|---|
sh:targetClass <C> | Every subject with rdf:type <C> (including RDFS subclasses of <C> when the hierarchy is available) |
sh:targetNode <N> | The specific subject <N> |
sh:targetSubjectsOf <P> | Every subject that currently has predicate <P> |
sh:targetObjectsOf <P> | Every node that currently appears as the object of <P> |
implicit (shape a rdfs:Class) | A shape that is also a class targets its own instances — no explicit target needed |
See Predicate-target shapes for notes on how the staged-path validator discovers focus nodes for sh:targetSubjectsOf / sh:targetObjectsOf.
Property paths
sh:path is usually a single predicate, but it can also be a property path expression. The path is evaluated against the focus node to produce the set of value nodes the constraints then apply to — so sh:minCount, sh:datatype, sh:class, etc. all work over a path exactly as they do over a plain predicate.
| Path form | Turtle syntax | Reaches |
|---|---|---|
| Predicate | sh:path ex:knows | objects of ex:knows |
| Inverse | sh:path [ sh:inversePath ex:parent ] | subjects that point at the focus via ex:parent (works over any path: [ sh:inversePath ( ex:a ex:b ) ] reaches nodes two hops upstream) |
| Sequence | sh:path ( ex:knows schema:name ) | names of the people the focus knows |
| Alternative | sh:path [ sh:alternativePath ( ex:email ex:altEmail ) ] | values via either predicate |
| Zero-or-more | sh:path [ sh:zeroOrMorePath ex:parent ] | the focus and all transitive ex:parent ancestors |
| One-or-more | sh:path [ sh:oneOrMorePath ex:parent ] | all transitive ex:parent ancestors (excludes the focus) |
| Zero-or-one | sh:path [ sh:zeroOrOnePath ex:parent ] | the focus and its direct parents |
These nest: sh:path ( [ sh:inversePath ex:parent ] schema:name ) reaches the names of the focus’s children.
# Every Parent must have at least one child (something points at it via ex:parent),
# and each place a Person knows-of must be named.
ex:ParentShape a sh:NodeShape ;
sh:targetClass ex:Parent ;
sh:property [
sh:path [ sh:inversePath ex:parent ] ;
sh:minCount 1 ;
sh:message "A Parent must have at least one child"
] .
ex:SocialiteShape a sh:NodeShape ;
sh:targetClass ex:Socialite ;
sh:property [
sh:path ( ex:knows schema:name ) ;
sh:datatype xsd:string ;
sh:minCount 1
] .
In JSON-LD, a sequence path is written with @list, and the blank-node forms are written as nested objects:
{
"@id": "ex:SocialiteShape",
"@type": "sh:NodeShape",
"sh:targetClass": { "@id": "ex:Socialite" },
"sh:property": [{
"sh:path": { "@list": [ { "@id": "ex:knows" }, { "@id": "schema:name" } ] },
"sh:datatype": { "@id": "xsd:string" },
"sh:minCount": 1
}, {
"sh:path": { "sh:inversePath": { "@id": "ex:parent" } },
"sh:minCount": 1
}]
}
Not supported: the inverse of a composite path (e.g. [ sh:inversePath ( ex:a ex:b ) ]). sh:inversePath may only wrap a single predicate. An unsupported or unresolvable path doesn’t silently pass — it produces a violation whenever the owning shape validates a focus node, so any transaction touching data that shape targets is rejected with a clear message. A broken path only affects the shape’s own targets, not unrelated writes.
Constraint patterns
Cardinality — required and multi-valued
ex:ArticleShape a sh:NodeShape ;
sh:targetClass ex:Article ;
sh:property [ sh:path ex:title ; sh:minCount 1 ; sh:maxCount 1 ] ;
sh:property [ sh:path ex:tag ; sh:minCount 1 ] .
Datatype
ex:ProductShape a sh:NodeShape ;
sh:targetClass ex:Product ;
sh:property [ sh:path ex:price ; sh:datatype xsd:decimal ] ;
sh:property [ sh:path ex:inStock ; sh:datatype xsd:boolean ] .
Numeric ranges
ex:OrderShape a sh:NodeShape ;
sh:targetClass ex:Order ;
sh:property [
sh:path ex:quantity ;
sh:datatype xsd:integer ;
sh:minInclusive 1 ;
sh:maxInclusive 10000
] .
Available: sh:minInclusive, sh:maxInclusive, sh:minExclusive, sh:maxExclusive.
String patterns and length
ex:UserShape a sh:NodeShape ;
sh:targetClass ex:User ;
sh:property [
sh:path ex:username ;
sh:datatype xsd:string ;
sh:minLength 3 ;
sh:maxLength 32 ;
sh:pattern "^[a-zA-Z0-9_]+$"
] .
sh:pattern accepts an optional sh:flags string (e.g. "i" for case-insensitive).
Language constraints
sh:languageIn restricts values to language-tagged literals whose tag matches
one of the given basic language ranges ("en" also matches "en-US", per
SPARQL langMatches); untagged values violate. sh:uniqueLang true forbids
two values of the property from sharing a language tag.
ex:LabelShape a sh:NodeShape ;
sh:targetClass ex:Labeled ;
sh:property [
sh:path ex:label ;
sh:languageIn ( "en" "fr" ) ;
sh:uniqueLang true
] .
Node kind
ex:RefShape sh:property [
sh:path ex:owner ;
sh:nodeKind sh:IRI
] .
Values: sh:IRI, sh:BlankNode, sh:Literal, sh:BlankNodeOrIRI, sh:BlankNodeOrLiteral, sh:IRIOrLiteral.
Enumerated values
ex:TaskShape a sh:NodeShape ;
sh:targetClass ex:Task ;
sh:property [
sh:path ex:status ;
sh:in ( "todo" "in-progress" "review" "done" )
] .
sh:hasValue requires a specific value to be present.
Class constraint (with RDFS subclass reasoning)
ex:OrderShape a sh:NodeShape ;
sh:targetClass ex:Order ;
sh:property [
sh:path ex:customer ;
sh:class schema:Person ;
sh:minCount 1
] .
Each value of ex:customer must have rdf:type schema:Person — or rdf:type of any class that is rdfs:subClassOf* schema:Person. See RDFS subclass reasoning for sh:class.
Pair constraints — comparing two properties
ex:EventShape a sh:NodeShape ;
sh:targetClass ex:Event ;
sh:property [
sh:path ex:startYear ;
sh:lessThan ex:endYear
] ;
sh:property [
sh:path ex:primaryEmail ;
sh:disjoint ex:secondaryEmail
] .
| Constraint | Semantic |
|---|---|
sh:equals <P> | Value sets for this path and <P> must be identical |
sh:disjoint <P> | Value sets must not overlap |
sh:lessThan <P> | Every value on this path must be strictly less than every value of <P> |
sh:lessThanOrEquals <P> | Every value on this path must be ≤ every value of <P> |
Logical constraints
ex:ContactShape a sh:NodeShape ;
sh:targetClass ex:Contact ;
sh:or (
[ sh:property [ sh:path schema:email ; sh:minCount 1 ] ]
[ sh:property [ sh:path schema:telephone ; sh:minCount 1 ] ]
) .
Available: sh:not, sh:and, sh:or, sh:xone.
Shape-based constraints (sh:node)
sh:node validates a node against another node shape. On a property shape it
applies to each value; directly on a node shape it applies to the focus node
itself. The referenced shape is usually targetless — it fires only where it is
referenced.
ex:AddressShape a sh:NodeShape ;
sh:property [ sh:path ex:postalCode ; sh:minCount 1 ] .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [
sh:path ex:address ;
sh:node ex:AddressShape
] .
Recursive references are safe: a shape may reference itself (directly or via a
chain), and validation over cyclic data (e.g. a mutual ex:knows graph)
terminates — a node already being validated against a shape higher in the
evaluation is assumed conforming, matching common SHACL engine behavior.
Qualified value shapes
sh:qualifiedValueShape counts how many values conform to a shape and checks
the count against sh:qualifiedMinCount / sh:qualifiedMaxCount — unlike
sh:node, values that don’t conform are fine as long as enough do.
ex:TeamShape a sh:NodeShape ;
sh:targetClass ex:Team ;
sh:property [
sh:path ex:member ;
sh:qualifiedValueShape ex:BadgedMemberShape ;
sh:qualifiedMinCount 1
] .
sh:qualifiedValueShapesDisjoint true additionally excludes values that
conform to a sibling qualified shape — e.g. a crew needing one pilot and one
navigator as distinct members rejects a single member holding both roles.
Constraints on the node itself
Value constraints declared directly on a node shape (without sh:path) apply
to the focus node. Combined with a predicate target this restricts which nodes
may appear in a position:
# Only ex:active / ex:inactive may be used as an ex:status value.
ex:StatusShape a sh:NodeShape ;
sh:targetObjectsOf ex:status ;
sh:in ( ex:active ex:inactive ) .
Deactivating a shape
sh:deactivated true turns a shape off without deleting it — it stops firing
for its targets and is treated as conforming when referenced via sh:node or
logical constraints.
Closed shapes
ex:StrictPersonShape a sh:NodeShape ;
sh:targetClass ex:StrictPerson ;
sh:closed true ;
sh:ignoredProperties ( rdf:type ) ;
sh:property [ sh:path schema:name ; sh:minCount 1 ] .
A closed shape forbids any property not explicitly declared (or listed in sh:ignoredProperties). Per the SHACL spec, rdf:type is not implicitly ignored — a closed shape with sh:targetClass (whose instances necessarily carry rdf:type) must list it in sh:ignoredProperties, as above.
RDFS entailment in enforcement
SHACL enforcement applies RDFS subclass and subproperty inference always — no configuration needed:
- Targets:
sh:targetClass ex:Employeealso fires for instances of anyrdfs:subClassOf* ex:Employeeclass;sh:targetSubjectsOf/sh:targetObjectsOf ex:phonealso match subjects/objects of anyrdfs:subPropertyOf* ex:phoneproperty. - Paths: a constraint on
sh:path schema:namealso governs values asserted via any subproperty ofschema:name(including through sequence/inverse/alternative path steps and pair constraints).
The hierarchy used is the committed state, kept current automatically:
commits that assert or retract rdfs:subClassOf / rdfs:subPropertyOf
invalidate a shared cache that rebuilds lazily; all other commits pay
nothing. One consequence worth knowing: schema asserted in the same
transaction as data does not entail for that transaction — commit the
schema first, then the data (two transactions).
Policy enforcement applies the same inference: f:onClass policies govern
subclass instances and f:onProperty policies govern subproperties — see
the policy cookbook.
RDFS subclass reasoning for sh:class
sh:class honors rdfs:subClassOf. Example:
ex:Novelist rdfs:subClassOf schema:Person .
ex:pratchett rdf:type ex:Novelist .
ex:BookShape sh:property [
sh:path ex:author ;
sh:class schema:Person
] .
A book whose ex:author is ex:pratchett conforms — ex:pratchett is a schema:Person via rdfs:subClassOf.
Fluree resolves this in two tiers:
- Fast path: the ledger’s indexed schema hierarchy (
SchemaHierarchy). Expanded at engine build time so same-class and descendant-class matches are O(1) hashmap hits. - Live fallback: when the subclass relation was asserted in the current transaction (or any earlier unindexed commit), the fast path misses. The engine then walks
rdfs:subClassOfvia a BFS on the database’s SPOT index. This walk is scoped to the default graph regardless of the subject’s own graph — matching howSchemaHierarchyis built and preventing cross-graph issues.
Predicate-target shapes
sh:targetSubjectsOf(P) and sh:targetObjectsOf(P) depend on the current state of the database — a subject is a focus node iff it actually has (or is referenced by) predicate P in the post-transaction view.
Fluree does not precompute target hints from staged flakes. Instead, for each focus node being validated, the engine does a bounded existence check against the post-state:
sh:targetSubjectsOf(P)→ SPOT range query(focus, P, _). Non-empty → shape applies.sh:targetObjectsOf(P)→ OPST range query(_, P, focus). Non-empty → shape applies.
This means:
- A base-state
(alice, ex:ssn, "123")makessh:targetSubjectsOf(ex:ssn)fire on alice even when this transaction only retractsex:name. - A retraction-only transaction that removes the last matching edge means the shape no longer applies — the post-state check returns empty.
- The check is bounded by the number of predicate-targeted shapes in the cache, not the data size.
Ref-objects of asserted flakes are pulled into the focus set for their graph, so newly-introduced inbound edges trigger validation of the referenced node.
Per-graph configuration
Each named graph can have its own f:shaclEnabled and f:validationMode via f:graphOverrides:
@prefix f: <https://ns.flur.ee/db#> .
@prefix ex: <http://example.org/> .
GRAPH <urn:fluree:mydb:main#config> {
<urn:config:main> a f:LedgerConfig ;
# Ledger-wide: SHACL on, reject on violation.
f:shaclDefaults [
f:shaclEnabled true ;
f:validationMode f:ValidationReject ;
f:overrideControl f:OverrideAll
] ;
# Per-graph: ex:scratch has SHACL off; ex:audit uses warn mode.
f:graphOverrides
[ a f:GraphConfig ;
f:targetGraph ex:scratch ;
f:shaclDefaults [ f:shaclEnabled false ]
],
[ a f:GraphConfig ;
f:targetGraph ex:audit ;
f:shaclDefaults [ f:validationMode f:ValidationWarn ]
] .
}
With this config:
- A violating write to the default graph is rejected (ledger-wide
Reject). - A violating write to
ex:scratchpasses without validation (graph disabled). - A violating write to
ex:auditpasses but emits atracing::warn!(Warnmode). - A single multi-graph transaction can mix modes: reject-bucket violations fail the txn; warn-bucket violations get logged.
Monotonicity
Per-graph configs can only tighten the ledger-wide posture:
| Ledger-wide | Per-graph | Effective |
|---|---|---|
enabled: false, OverrideNone | enabled: true | disabled (OverrideNone blocks per-graph) |
enabled: true, OverrideAll | enabled: false | disabled for that graph |
mode: warn, OverrideAll | mode: reject | reject for that graph |
See Override control for the full ruleset.
Storing shapes in a named graph
f:shapesSource points the shape compiler at a specific graph. Useful when you want schema / shapes isolated from data — even the config graph itself can be used as a shape source.
@prefix f: <https://ns.flur.ee/db#> .
GRAPH <urn:fluree:mydb:main#config> {
<urn:config:main> a f:LedgerConfig ;
f:shaclDefaults [
f:shaclEnabled true ;
f:shapesSource [
a f:GraphRef ;
f:graphSource [ f:graphSelector <http://example.org/shapes> ]
]
] .
}
Semantics:
f:shapesSourceis authoritative, not additive: when set, shapes come exclusively from the configured graph. Shapes in the default graph are ignored.f:shapesSourceis non-overridable — it can only be set in the config graph, not via transaction/query-time options.- Use
f:graphSelector f:defaultGraphto explicitly point at the default graph (same as omittingf:shapesSource). f:shapesSourcealso supports cross-ledger references — setf:ledgeron the innerf:graphSourceto compile shapes from a different ledger at validation time. See Cross-ledger governance — Cross-ledger SHACL shapes for the end-to-end pattern.
Shared value-sets with sh:class
sh:class is the natural way to model a controlled value-set — e.g. a fixed list of US states — as an extensible enumeration: each allowed value is an instance of a class, and adding a new value means inserting one triple rather than editing the shape (contrast sh:in, which bakes the list into the shape). Put the value-set vocabulary in the same graph as the shapes (f:shapesSource), and it is honoured even when the referencing records live in a different graph:
@prefix f: <https://ns.flur.ee/db#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix ex: <http://example.org/> .
GRAPH <http://example.org/shapes> {
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [ sh:path ex:homeState ; sh:class ex:USState ] .
# The value-set vocabulary lives alongside the shapes.
ex:illinois a ex:USState .
ex:iowa a ex:USState .
}
With f:shapesSource pointing at <http://example.org/shapes>, a record written to any graph passes validation when its ex:homeState is one of the declared states, and is rejected otherwise. Adding a state later (ex:ohio a ex:USState) requires no shape change.
Semantics and limits:
- Membership graph = focus data graph ∪ the
f:shapesSourcegraph. A value is an instance of the class if it is typed so in either the record’s own graph or the vocabulary graph. When shapes live in the default graph, this is just an ordinary local lookup. - Per-transaction caching. Repeated references to the same value within one transaction are memoized — bulk-inserting many records that share a state pays the membership lookup once.
- Cross-ledger too. When
f:shapesSourceis cross-ledger (f:ledger), the controlled vocabulary lives in the model ledger alongside the shapes, and membership is resolved by querying that model ledger live (pinned at its latest committed state at transaction time). So a shared governance model can hold shapes and the value-sets they reference, and many data ledgers can point at it.
Inline shapes per transaction
In addition to shapes stored in a ledger, a transaction can supply
inline shapes via the opts.shapes field. The shapes are
enforced only for that one transaction and never written into the
ledger.
{
"@context": {"ex": "http://example.org/ns/"},
"@id": "ex:alice",
"@type": "ex:Person",
"opts": {
"shapes": {
"@context": {
"ex": "http://example.org/ns/",
"sh": "http://www.w3.org/ns/shacl#",
"xsd": "http://www.w3.org/2001/XMLSchema#"
},
"@graph": [
{
"@id": "ex:PersonShape",
"@type": "sh:NodeShape",
"sh:targetClass": {"@id": "ex:Person"},
"sh:property": {"@id": "ex:pshape_name"}
},
{
"@id": "ex:pshape_name",
"sh:path": {"@id": "ex:name"},
"sh:minCount": 1,
"sh:datatype": {"@id": "xsd:string"}
}
]
}
}
}
Semantics:
- Additive with the configured source. Inline shapes enforce
alongside whatever
f:shapesSourceresolves to. A subject must satisfy every shape from both sources. Note thatf:shapesSourceis itself singular — itsf:graphSourceis either local (nof:ledger) or cross-ledger (withf:ledger), not both at the same time. Inline shapes don’t change that; they layer on top of whichever variant is configured. - Transient. The shapes never appear in the ledger’s data and
vanish after the transaction completes. The next transaction
without
opts.shapesruns without them. - Gated by config. If
f:shaclEnabled false(or no graph is enabled), inline shapes do not bypass that posture — operator config wins. To use inline shapes on a fresh ledger with no config, the shapes-exist heuristic enables validation automatically. - No audit trail. Because inline shapes don’t persist, it
isn’t possible to reconstruct “which shapes validated which
commit” from ledger history. If auditability matters, store
shapes in a
f:shapesSourcegraph instead.
Use cases that fit well: ad-hoc shape testing before committing
to f:shapesSource, per-tenant validation layers in an
application server, request-scoped governance that should not
become part of permanent ledger state.
Validation modes
f:ValidationReject(default): on any violation, the transaction fails withShaclViolation(report). The formatted report lists each violation’s focus node, property path, and message.f:ValidationWarn: violations are logged viatracing::warn!and the transaction proceeds. Any non-violation error from the SHACL pipeline (compile failure, range-scan failure) still propagates — Warn mode never silently admits a broken validation pipeline.
Working with shapes across write surfaces
SHACL validation runs consistently on every write surface:
- JSON-LD / SPARQL transactions (
fluree insert,fluree upsert,fluree update) - Turtle / TriG ingest (
fluree insert-turtle,stage_turtle_insert) - Commit replay (
push_commits_with_handle, followers applying upstream commits)
All three routes go through the same post-stage helper, so the ledger’s configured SHACL posture (enable/disable, mode, per-graph, shapes source) applies uniformly.
Validation reports (fluree validate)
Transaction-time enforcement rejects (or warns about) new writes.
fluree validate answers the complementary questions: is my existing data
clean? and is this source file clean before I import it? It produces a
W3C-shaped sh:ValidationReport instead of an error:
fluree validate mydb # ledger vs its attached shapes
fluree validate mydb --shacl proposed.ttl # trial stricter shapes (replaces attached)
fluree validate data.ttl --shacl shapes.ttl # standalone file, ephemeral in-memory ledger
fluree validate data.ttl --format turtle # W3C report as Turtle (also: jsonld)
Ad-hoc shapes (--shacl) replace the attached shapes by default so
“does this data conform to these rules?” is answered exactly;
--include-attached unions both sets. Exit codes make it CI-friendly:
0 = conforms, 1 = findings at/above --fail-on (default violation).
See the fluree validate reference.
The same core is served over HTTP as
GET|POST /validate/{ledger}
(JSON summary by default; Accept: application/ld+json or text/turtle
for the W3C report), and exposed in Rust as fluree_db_api::validate —
Fluree::validate_ledger(alias, &ValidateOptions) returns a ValidateReport
with per-result constraint-component IRIs, severities, and messages, plus
to_jsonld() / to_turtle() serializers.
Not yet supported
sh:sparql(SPARQL-based constraints).
These are tracked in the SHACL compliance effort.
Shapes are data
Because shapes live as regular RDF in your ledger:
- Time-travelable —
@atTquery any shape’s history to see what validation was in effect at a given commit. - Versionable —
delete/insertconstraints through ordinary transactions. - Queryable —
SELECT ?shape ?target WHERE { ?shape sh:targetClass ?target }. - Branchable — test new constraints on a branch; merge when verified.
Best practices
- Start with
sh:minCount— missing-value bugs are the most common data quality issue. - Incremental rollout — deploy shapes in
f:ValidationWarnmode first. Watch the logs for a sprint, then flip tof:ValidationReject. - Per-graph scratch zones — for experimentation, disable SHACL on a named graph so exploratory transactions don’t fail your CI.
sh:messageeverywhere — custom messages are what end users see when a transaction is rejected. Invest in them early.f:shapesSourcefor schema hygiene — keep shapes out of user data graphs so deletes / retractions on user data can’t accidentally touch your schema.
Related documentation
- Setting Groups — SHACL — Configuration reference for
f:shaclDefaults - Override Control — Per-graph / query-time override rules
- Writing Config Data — How to transact into the config graph