fix(shaclgen): emit sh:pattern for pattern constraints inside any_of - #13
Open
jdsika wants to merge 1 commit into
Open
fix(shaclgen): emit sh:pattern for pattern constraints inside any_of#13jdsika wants to merge 1 commit into
jdsika wants to merge 1 commit into
Conversation
jdsika
force-pushed
the
fix/shaclgen-any-of-pattern
branch
3 times, most recently
from
May 7, 2026 12:13
6630206 to
f9c16d5
Compare
jdsika
force-pushed
the
fix/shaclgen-any-of-pattern
branch
6 times, most recently
from
May 12, 2026 10:53
d1652e5 to
bfa9ea6
Compare
jdsika
force-pushed
the
fix/shaclgen-any-of-pattern
branch
from
June 9, 2026 15:20
bfa9ea6 to
2c6b519
Compare
jdsika
force-pushed
the
fix/shaclgen-any-of-pattern
branch
from
July 3, 2026 11:51
2c6b519 to
09a4fbd
Compare
rmessaou
force-pushed
the
fix/shaclgen-any-of-pattern
branch
2 times, most recently
from
July 8, 2026 08:49
a578620 to
d736a35
Compare
3 tasks
jdsika
force-pushed
the
fix/shaclgen-any-of-pattern
branch
from
September 8, 2026 09:02
d736a35 to
0c334af
Compare
The SHACL generator translated any_of branches by dispatching
solely on `any.range` (class, type, enum, or simple datatype).
If a branch specified `pattern:` — either alone or combined
with a range — the constraint was silently dropped, producing
an empty blank node `[ ]` (trivially satisfied) instead of the
intended `[ sh:pattern "..." ]`.
This is a problem for schemas that use pattern alternatives in
`any_of`, such as the SPDX license field where valid values are
either members of a fixed enum (SPDX identifiers), IRIs, or
custom identifiers matching the LicenseRef- pattern defined in
SPDX Specification v2.3 Annex D (ABNF: license-ref =
["DocumentRef-"(idstring)":"]"LicenseRef-"(idstring)).
The fix adds a single check after the range dispatch:
if any.pattern:
g.add((range_list[-1], SH.pattern, Literal(any.pattern)))
This correctly handles:
- Pattern-only branches (no range): node gets only sh:pattern
- Range + pattern branches: node gets both sh:datatype and sh:pattern
- Range-only branches (no pattern): unchanged behaviour
The test suite now includes a dedicated schema exercising all
three cases, with assertions on both the generated RDF triples
and pyshacl validation of conforming/non-conforming data.
Signed-off-by: Carlo van Driesten <carlo.van-driesten@bmw.de>
jdsika
force-pushed
the
fix/shaclgen-any-of-pattern
branch
from
September 11, 2026 14:02
0c334af to
7394735
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The SHACL generator (
shaclgen.py) currently ignorespatternconstraints specified insideany_ofbranches. When a slot usesany_ofwith a branch that haspattern:(with or withoutrange:), the generated SHACL produces an empty blank node[ ]instead of[ sh:pattern "..." ].This means any value trivially satisfies the branch (an empty node has no constraints), making the
sh:orvalidation too permissive.Root Cause
In
shaclgen.py(thefor any in s.any_of:loop, ~L325–373), branch translation dispatches solely onany.range:sh:classsh:datatypesh:inadd_simple_data_type(func, None))The loop never reads
any.patternfrom theAnonymousSlotExpression, so pattern-only branches (no range) produce empty constraint nodes, and range+pattern branches lose the pattern constraint.Note: the top-level
s.patterncheck (~L394) sits in theelse:arm ofif s.any_of:, so it only runs whenany_ofis absent and cannot cover patterns inside branches.Fix
After the if/elif/else dispatch that creates each branch BNode, add:
This correctly handles all three cases:
sh:pattern→[ sh:pattern "^..." ]sh:datatypeandsh:pattern→[ sh:datatype xsd:string ; sh:pattern "^..." ]This mirrors the existing top-level handling (
if s.pattern: prop_pv(SH.pattern, Literal(s.pattern))), so both paths emitsh:patternconsistently.Test Coverage
A 3-class test schema (
tests/linkml/test_generators/input/shaclgen/any_of_pattern.yaml) exercises:PatternOnlyBranch: enum + URI + pattern-only (no range) inany_ofRangeWithPattern:range: string+patternon same branchMixedBranches: combination of range-only, pattern-only, and range+pattern branchesTwo tests consume it:
test_any_of_with_pattern()— asserts on the generated RDF triples (which branch node carriessh:pattern, and that range-only branches do not).test_any_of_with_pattern_pyshacl_end_to_end()— behavioural guard: runspyshaclover conforming and non-conforming instances for all three classes. Without the fix a pattern-only branch serialises as an empty shape[ ], which every value node trivially satisfies, sosh:oraccepts anything and the non-conforming assertions fail.Both tests were verified to fail on
mainwithout the one-line change and pass with it. All existing tests continue to pass.Real-World Use Case
This fix is needed for SPDX license validation in the Gaia-X / ENVITED-X ecosystem. The
gx:licenseslot usesany_ofto accept:range: uri)^LicenseRef-[a-zA-Z0-9\-\.]+$(pattern-only branch)Without this fix, branch (3) generates an empty SHACL node that accepts anything, making validation meaningless. The corresponding upstream model change is tracked in service-characteristics fix/spdx-license-ref-pattern.
Specification References
The ordinal string value of the slot must match the given patternholds if at least one of the expressions holdsh:patternis a value-node string constraint, so it is legal (and idiomatic) on the branch shapes insidesh:or; for literals it matches the lexical form, for IRIs the IRI string.This fix was developed and validated using the Gaia-X credential model as a real-world test case. AI-assisted development (GitHub Copilot).