Skip to main content

Exercise 3: Semantic validation with SHACL

JSON Schema validates structure: types, required fields, string formats, value ranges. But there are constraints that JSON Schema cannot express, such as "the value of field X must be less than the value of field Y," or "this resource must be linked to at least one instance of class Z."

These are semantic constraints — they depend on the meaning of the fields, not just their position in a document. Because the JSON-LD context established in Exercise 2 maps each field to a URI, the data can be represented as an RDF graph. And over an RDF graph, SHACL (Shapes Constraint Language) can express exactly these kinds of constraints.

A block's SHACL rules live in rules.shacl. The postprocessor applies them to the uplifted RDF graph of every example and records whether each shape passes or fails.

Step 1: Create the exercise files

Create the directory _sources/exercise3/ and add the following files.

bblock.json:

{
"name": "Exercise 3: Semantic validation with SHACL",
"abstract": "Adding a SHACL rule that enforces a cross-property constraint on the RDF graph.",
"itemClass": "schema",
"status": "under-development",
"dateTimeAddition": "2024-01-01T00:00:00Z",
"version": "0.1",
"dateOfLastChange": "2024-01-01"
}

schema.yaml (same schema as before):

$schema: https://json-schema.org/draft/2020-12/schema
description: My example schema
type: object
properties:
a:
type: string
format: uri
b:
type: number
c:
type: number
required:
- a
- b

context.jsonld — note the namespace is different from Exercise 2 (http://example.org/ns1/ rather than http://example.com/mythings/):

{
"@context": {
"mynamespace": "http://example.org/ns1/",
"a": "@type",
"b": "https://example.org/my-bb-model/b",
"c": "https://example.org/my-bb-model/c"
}
}

examples.yaml:

examples:
- title: A minimal object
snippets:
- language: json
code: |
{
"a": "mynamespace:aThing",
"b": 23,
"c": 1
}

rules.shacl — a constraint that c must be less than b, but with no target class declared:

@prefix sh:          <http://www.w3.org/ns/shacl#> .
@prefix mynamespace: <http://example.org/ns1/> .
@prefix ns1: <https://example.org/my-bb-model/> .

<#testValues>
a sh:NodeShape ;
sh:message "C must be less than B" ;
sh:property [ sh:path ns1:c ;
sh:lessThan ns1:b ]
.

The shape defines a constraint — c must be less than b — but it has no target. Without sh:targetClass, SHACL has no way to know which nodes in the RDF graph to apply the shape to, so the constraint is never checked.

Run the postprocessor. Navigate to the Exercise 3 block and open the Examples tab. The example passes — but it passes because the rule is never applied, not because the data is valid. Open the validation report (linked from the banner on the block's main page) and look at the SHACL section: the #testValues shape is listed but shows no targeted nodes — confirming that the constraint ran against nothing.

Step 2: Add the target class and rebuild

Add sh:targetClass mynamespace:aThing ; to the shape in rules.shacl:

<#testValues>
a sh:NodeShape ;
sh:targetClass mynamespace:aThing ;
sh:message "C must be less than B" ;
sh:property [ sh:path ns1:c ;
sh:lessThan ns1:b ]
.

Rerun the postprocessor and navigate to the Exercise 3 block in the viewer. Open the Examples tab. The example has "b": 23 and "c": 1, so c < b — the constraint passes, and the example validates. Open the validation report and look at the SHACL section: the blank node representing the example is now listed as a target of the #testValues shape, with the sh:lessThan constraint shown as passing. This is the confirmation that the rule is genuinely active and checking real data.

To verify the rule is working, temporarily modify your inline snippet in examples.yaml to set "c": 30 (greater than b=23) and rebuild. The validation result will flip to a failure with the shape message. Restore the original value when you are done.

How it works

How SHACL targets nodes

sh:targetClass mynamespace:aThing tells SHACL to apply this shape to every node in the RDF graph that has the RDF type mynamespace:aThing. In the SHACL file, mynamespace is declared as <http://example.org/ns1/>, so the target class is http://example.org/ns1/aThing.

In the example JSON, "a": "mynamespace:aThing" is interpreted using the context.jsonld, where "a": "@type" and "mynamespace": "http://example.org/ns1/" — so the value expands to the same URI, http://example.org/ns1/aThing. The blank node representing the example object is therefore assigned that type, and the shape targets it.

Without the sh:targetClass, the shape exists but is never applied. SHACL requires an explicit target — it does not apply shapes to all nodes by default.

The sh:lessThan constraint

sh:property [ sh:path ns1:c ; sh:lessThan ns1:b ]

This property constraint says: for every node matched by the shape, the value at path ns1:c must be less than the value at path ns1:b. Because context.jsonld maps c to ns1:c and b to ns1:b, the uplifted RDF carries the numeric values at those predicates, and SHACL can compare them.

JSON Schema vs SHACL

JSON SchemaSHACL
Operates onRaw JSONUplifted RDF graph
Can expressTypes, required fields, formats, rangesCross-property relationships, class membership, cardinality over graph links
RequiresNothingJSON-LD context (to produce the RDF graph)
Triggered byAny JSON snippetJSON snippets after semantic uplift

The two validation layers are complementary. Use JSON Schema for structural correctness, SHACL for semantic correctness.


Next: Exercise 4 — Profiling a block and writing unit tests.