Common Design Pattern Conversions¶
This page shows how to translate four common structural transformations using SHACL Bridges. Each example provides:
- A Mermaid diagram visualising the before/after graph shapes
- A bridge YAML file
- A minimal source RDF data file
- The SPARQL CONSTRUCT query that is embedded in the generated SHACL shape
- The diff output — the triples the bridge adds
Pattern 1 — Chain → Diamond¶
Chain: A → B → C → D (a single linear path)
Diamond: A → B, A → C, B → D, C → D (two parallel branches converging)
This is the classic "split-and-merge" transformation: one linear dependency chain is replaced by two independent sub-processes that both feed into the same final node.
flowchart LR
subgraph Source["Source pattern (chain)"]
A[A] ==> B[B]
B ==> C[C]
C ==> D[D]
end
subgraph Target["Target pattern (diamond)"]
A2(A) --> B2(B)
A2 --> C2(C)
B2 --> D2(D)
C2 --> D2
end
A -.......->|SHACL_bridge| A2
B -.......->|SHACL_bridge| B2
C -.......->|SHACL_bridge| C2
D -.......->|SHACL_bridge| D2
bridge.yaml¶
metadata:
title: "Chain to Diamond"
mapping_justification: "semapv:ManualMappingCuration"
prefixes:
ex: "http://example.org/pattern1#"
source_pattern:
root: "ex:A"
triples:
- ["ex:A", "ex:next", "ex:B"]
- ["ex:B", "ex:next", "ex:C"]
- ["ex:C", "ex:next", "ex:D"]
target_pattern:
triples:
- ["ex:A", "ex:branchLeft", "ex:B"]
- ["ex:A", "ex:branchRight", "ex:C"]
- ["ex:B", "ex:feeds", "ex:D"]
- ["ex:C", "ex:feeds", "ex:D"]
class_map:
- source: "ex:A"
target: "ex:A"
justification: "semapv:ManualMappingCuration"
comment: "Root node, same class in target"
- source: "ex:B"
target: "ex:B"
justification: "semapv:ManualMappingCuration"
- source: "ex:C"
target: "ex:C"
justification: "semapv:ManualMappingCuration"
- source: "ex:D"
target: "ex:D"
justification: "semapv:ManualMappingCuration"
comment: "Terminal node — receives two incoming edges in target"
data.ttl (source instances)¶
@prefix ex: <http://example.org/pattern1#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
ex:a1 a ex:A .
ex:b1 a ex:B .
ex:c1 a ex:C .
ex:d1 a ex:D .
ex:a1 ex:next ex:b1 .
ex:b1 ex:next ex:c1 .
ex:c1 ex:next ex:d1 .
Generated SPARQL CONSTRUCT (embedded in SHACL)¶
CONSTRUCT {
?this rdf:type ex:A .
?var_b rdf:type ex:B .
?var_c rdf:type ex:C .
?var_d rdf:type ex:D .
?this ex:branchLeft ?var_b .
?this ex:branchRight ?var_c .
?var_b ex:feeds ?var_d .
?var_c ex:feeds ?var_d .
}
WHERE {
?this rdf:type ex:A .
?this ex:next ?var_b .
?var_b rdf:type ex:B .
?var_b ex:next ?var_c .
?var_c rdf:type ex:C .
?var_c ex:next ?var_d .
?var_d rdf:type ex:D .
}
diff.ttl (bridge output — new triples only)¶
@prefix ex: <http://example.org/pattern1#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
ex:a1 ex:branchLeft ex:b1 .
ex:a1 ex:branchRight ex:c1 .
ex:b1 ex:feeds ex:d1 .
ex:c1 ex:feeds ex:d1 .
Note
The rdf:type triples are already present in the source graph (asserted or inferred
by RDFS), so they do not appear in the diff. Only the newly constructed relation
triples are added.
Pattern 2 — Hierarchy Rearrangement¶
Before: A → B → C (B is a child of A, C is a grandchild)
After: A → C → B (C is promoted one level; B becomes a child of C)
Use this when a target ontology has a different containment / specialisation hierarchy than the source.
flowchart LR
subgraph Source["Source pattern"]
A[A] ==> B[B]
B ==> C[C]
end
subgraph Target["Target pattern"]
A2(A) --> C2(C)
C2 --> B2(B)
end
A -.......->|SHACL_bridge| A2
B -.......->|SHACL_bridge| B2
C -.......->|SHACL_bridge| C2
bridge.yaml¶
metadata:
title: "Hierarchy Rearrangement"
mapping_justification: "semapv:ManualMappingCuration"
prefixes:
ex: "http://example.org/pattern2#"
source_pattern:
root: "ex:A"
triples:
- ["ex:A", "ex:contains", "ex:B"]
- ["ex:B", "ex:contains", "ex:C"]
target_pattern:
triples:
- ["ex:A", "ex:contains", "ex:C"]
- ["ex:C", "ex:contains", "ex:B"]
class_map:
- source: "ex:A"
target: "ex:A"
justification: "semapv:ManualMappingCuration"
- source: "ex:B"
target: "ex:B"
justification: "semapv:ManualMappingCuration"
comment: "B is demoted one level — becomes a child of C in the target"
- source: "ex:C"
target: "ex:C"
justification: "semapv:ManualMappingCuration"
comment: "C is promoted — becomes a direct child of A in the target"
data.ttl (source instances)¶
@prefix ex: <http://example.org/pattern2#> .
ex:a1 a ex:A .
ex:b1 a ex:B .
ex:c1 a ex:C .
ex:a1 ex:contains ex:b1 .
ex:b1 ex:contains ex:c1 .
Generated SPARQL CONSTRUCT¶
CONSTRUCT {
?this rdf:type ex:A .
?var_b rdf:type ex:B .
?var_c rdf:type ex:C .
?this ex:contains ?var_c .
?var_c ex:contains ?var_b .
}
WHERE {
?this rdf:type ex:A .
?this ex:contains ?var_b .
?var_b rdf:type ex:B .
?var_b ex:contains ?var_c .
?var_c rdf:type ex:C .
}
diff.ttl¶
Pattern 3 — Blank Node Insertion¶
Sometimes the target pattern requires an intermediate node that has no equivalent in
the source — for example, a reified relation or an anonymous grouping node.
SHACL Bridges uses the _:label syntax for blank nodes: one fresh blank node is created
per solution row, so each matched source subgraph gets its own intermediate instance.
flowchart LR
subgraph Source["Source pattern"]
A[A] ==> B[B]
end
subgraph Target["Target pattern (blank node mediator)"]
A2(A) --> BN(_:link)
BN --> B2(B)
end
A -.......->|SHACL_bridge| A2
B -.......->|SHACL_bridge| B2
Blank node semantics in SPARQL CONSTRUCT
In a SPARQL CONSTRUCT template, a blank-node label such as _:link generates a
new, distinct blank node for every solution row. This is the standard mechanism
for creating anonymous intermediary nodes during graph transformation.
bridge.yaml¶
metadata:
title: "Blank Node Insertion"
mapping_justification: "semapv:ManualMappingCuration"
prefixes:
ex: "http://example.org/pattern3#"
source_pattern:
root: "ex:A"
triples:
- ["ex:A", "ex:relatesTo", "ex:B"]
target_pattern:
triples:
- ["ex:A", "ex:hasLink", "_:link"]
- ["_:link", "ex:linksTo", "ex:B"]
class_map:
- source: "ex:A"
target: "ex:A"
justification: "semapv:ManualMappingCuration"
- source: "ex:B"
target: "ex:B"
justification: "semapv:ManualMappingCuration"
comment: "Blank node _:link is an anonymous mediator with no source equivalent"
Tip
Blank-node targets in class_map are not supported and not needed — the blank
node appears only in target_pattern.triples, not as a class_map target.
data.ttl (source instances)¶
@prefix ex: <http://example.org/pattern3#> .
ex:a1 a ex:A .
ex:b1 a ex:B .
ex:a1 ex:relatesTo ex:b1 .
ex:a2 a ex:A .
ex:b2 a ex:B .
ex:a2 ex:relatesTo ex:b2 .
Generated SPARQL CONSTRUCT¶
CONSTRUCT {
?this rdf:type ex:A .
?var_b rdf:type ex:B .
?this ex:hasLink _:link .
_:link ex:linksTo ?var_b .
}
WHERE {
?this rdf:type ex:A .
?this ex:relatesTo ?var_b .
?var_b rdf:type ex:B .
}
diff.ttl¶
@prefix ex: <http://example.org/pattern3#> .
ex:a1 ex:hasLink _:bn0 .
_:bn0 ex:linksTo ex:b1 .
ex:a2 ex:hasLink _:bn1 .
_:bn1 ex:linksTo ex:b2 .
Each source pair (a1, b1) and (a2, b2) produces its own distinct blank node
(_:bn0, _:bn1). The blank node labels in the diff file are assigned by the RDF
serialiser and are not meaningful beyond the file they appear in.
Pattern 4 — Instance Convergence via owl:sameAs¶
When two source classes represent the same real-world entity and you want the target
graph to reflect that identity, you assert owl:sameAs between the corresponding
target instances. An OWL reasoner will then merge all properties across the two
identity-linked nodes.
flowchart LR
subgraph Source["Source pattern"]
A[A] ==> B[B]
A ==> C[C]
end
subgraph Target["Target pattern"]
A2(A) --> B2(B)
A2 --> C2(C)
B2 -->|owl:sameAs| C2
end
A -.......->|SHACL_bridge| A2
B -.......->|SHACL_bridge| B2
C -.......->|SHACL_bridge| C2
When to use owl:sameAs
Use this pattern when B and C are separate individuals in the source that are
determined to be the same entity (e.g. through an external identifier or a
domain rule). The owl:sameAs assertion lets an OWL reasoner propagate all
properties of B to C and vice versa, effectively converging them into one node.
bridge.yaml¶
metadata:
title: "Instance Convergence via owl:sameAs"
mapping_justification: "semapv:ManualMappingCuration"
prefixes:
ex: "http://example.org/pattern4#"
owl: "http://www.w3.org/2002/07/owl#"
source_pattern:
root: "ex:A"
triples:
- ["ex:A", "ex:hasLeft", "ex:B"]
- ["ex:A", "ex:hasRight", "ex:C"]
target_pattern:
triples:
- ["ex:A", "ex:hasLeft", "ex:B"]
- ["ex:A", "ex:hasRight", "ex:C"]
- ["ex:B", "owl:sameAs", "ex:C"]
class_map:
- source: "ex:A"
target: "ex:A"
justification: "semapv:ManualMappingCuration"
- source: "ex:B"
target: "ex:B"
justification: "semapv:ManualMappingCuration"
comment: "Asserted to be the same entity as C in the target"
- source: "ex:C"
target: "ex:C"
justification: "semapv:ManualMappingCuration"
data.ttl (source instances)¶
@prefix ex: <http://example.org/pattern4#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
ex:a1 a ex:A .
ex:b1 a ex:B .
ex:c1 a ex:C .
ex:a1 ex:hasLeft ex:b1 .
ex:a1 ex:hasRight ex:c1 .
Generated SPARQL CONSTRUCT¶
CONSTRUCT {
?this rdf:type ex:A .
?var_b rdf:type ex:B .
?var_c rdf:type ex:C .
?this ex:hasLeft ?var_b .
?this ex:hasRight ?var_c .
?var_b owl:sameAs ?var_c .
}
WHERE {
?this rdf:type ex:A .
?this ex:hasLeft ?var_b .
?var_b rdf:type ex:B .
?this ex:hasRight ?var_c .
?var_c rdf:type ex:C .
}
diff.ttl¶
@prefix ex: <http://example.org/pattern4#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
ex:b1 owl:sameAs ex:c1 .
The relation triples (ex:hasLeft, ex:hasRight) were already present in the source
data, so only the owl:sameAs assertion is new.
Downstream reasoner required
The owl:sameAs triple alone does not merge the nodes in the diff graph.
You need an OWL reasoner (e.g. owlrl) applied to expanded.ttl to derive
all entailments. Pass --inference owlrl to shacl-bridges run to enable this.
Pattern 5 — Instance Split (Conflated Entity → BFO Entity + Role)¶
Some source ontologies conflate an entity and its role into a single class —
a common pattern when modelling agents: one class represents both the
independent continuant (the agent itself) and the role it plays.
BFO / OBI requires these to be separate individuals linked by
obo:RO_0000087 (bearer of).
The instance split transform mints a fresh IRI for the role instance
at query time using SPARQL's BIND(IRI(CONCAT(...))) mechanism.
The minted IRI is deterministic (suffix appended to the source IRI), so
running the bridge twice produces no duplicates.
flowchart LR
subgraph Source["Source pattern (conflated)"]
AE[AgenticEntity] ==> T[Task]
end
subgraph Target["Target pattern (BFO-aligned)"]
AG(Agent) -->|obo:RO_0000087| AR(AgentRole)
AR --> T2(Task)
end
AE -.......->|SHACL_bridge| AG
AE -..........->|"SHACL_bridge\n(suffix:_role)"| AR
T -.......->|SHACL_bridge| T2
IRI minting — suffix: form
derived_iri: "suffix:_role" appends the literal string _role to the
source instance IRI. So ex:researcherSmith becomes
ex:researcherSmith_role. The generated SPARQL is:
This is evaluated once per matched ?this, guaranteeing a 1-to-1
correspondence between source instances and minted role instances.
bridge.yaml¶
metadata:
title: "Agentic Entity Split (BFO-aligned)"
mapping_justification: "semapv:ManualMappingCuration"
prefixes:
ex: "http://example.org/pattern5#"
obo: "http://purl.obolibrary.org/obo/"
source_pattern:
root: "ex:AgenticEntity"
triples:
- ["ex:AgenticEntity", "ex:performsTask", "ex:Task"]
target_pattern:
triples:
- ["ex:Agent", "obo:RO_0000087", "ex:AgentRole"]
- ["ex:AgentRole", "ex:performsTask", "ex:Task"]
class_map:
# Regular (non-derived) entry — source instance keeps its IRI, becomes Agent
- source: "ex:AgenticEntity"
target: "ex:Agent"
justification: "semapv:ManualMappingCuration"
comment: "Entity side — retains the source instance IRI"
# Derived entry — a new AgentRole instance is minted as {source_iri}_role
- source: "ex:AgenticEntity"
target: "ex:AgentRole"
derived_iri: "suffix:_role"
justification: "semapv:ManualMappingCuration"
comment: "Role side — IRI minted as {source_iri}_role"
- source: "ex:Task"
target: "ex:Task"
justification: "semapv:ManualMappingCuration"
Two entries, same source
It is valid — and necessary for a split — to have two class_map entries
with the same source. The first (no derived_iri) is the primary
mapping that determines ?this's target type. The second (with
derived_iri) describes the newly minted sibling instance.
data.ttl (source instances)¶
@prefix ex: <http://example.org/pattern5#> .
ex:researcherSmith a ex:AgenticEntity .
ex:taskA a ex:Task .
ex:researcherSmith ex:performsTask ex:taskA .
ex:robotArm1 a ex:AgenticEntity .
ex:taskB a ex:Task .
ex:robotArm1 ex:performsTask ex:taskB .
Generated SPARQL CONSTRUCT¶
CONSTRUCT {
?this rdf:type ex:Agent .
?derived_AgentRole rdf:type ex:AgentRole .
?var_b rdf:type ex:Task .
?this obo:RO_0000087 ?derived_AgentRole .
?derived_AgentRole ex:performsTask ?var_b .
}
WHERE {
?this rdf:type ex:AgenticEntity .
?this ex:performsTask ?var_b .
?var_b rdf:type ex:Task .
BIND(IRI(CONCAT(STR(?this), "_role")) AS ?derived_AgentRole)
}
diff.ttl¶
@prefix ex: <http://example.org/pattern5#> .
@prefix obo: <http://purl.obolibrary.org/obo/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
# --- researcherSmith split ---
ex:researcherSmith rdf:type ex:Agent .
ex:researcherSmith_role rdf:type ex:AgentRole .
ex:researcherSmith obo:RO_0000087 ex:researcherSmith_role .
ex:researcherSmith_role ex:performsTask ex:taskA .
# --- robotArm1 split ---
ex:robotArm1 rdf:type ex:Agent .
ex:robotArm1_role rdf:type ex:AgentRole .
ex:robotArm1 obo:RO_0000087 ex:robotArm1_role .
ex:robotArm1_role ex:performsTask ex:taskB .
Each source AgenticEntity instance produces exactly one minted AgentRole
instance. The rdf:type ex:Task triples are not new (already in the source
graph), so they do not appear in the diff.
IRI stability
The suffix strategy works well when source IRIs are stable. If source IRIs are regenerated across runs (e.g. UUIDs assigned at ingest time), consider minting role IRIs from a stable identifier embedded in a data property rather than from the instance IRI itself.
Combining Patterns¶
These five building blocks can be composed:
- A chain → diamond followed by a blank node insertion on one branch.
- A hierarchy rearrangement that also introduces a sameAs convergence at the bottom level.
- An instance split whose minted role node also carries a blank node mediator to a third class.
In all cases the approach is the same: enumerate the full source_pattern and
target_pattern as S-P-O triples, provide the class alignment in class_map,
and add derived_iri entries wherever a new instance must be minted.
The tool handles variable assignment and SPARQL generation automatically.