Skip to content

fix(shaclgen): emit sh:pattern for pattern constraints inside any_of - #13

Open
jdsika wants to merge 1 commit into
mainfrom
fix/shaclgen-any-of-pattern
Open

fix(shaclgen): emit sh:pattern for pattern constraints inside any_of#13
jdsika wants to merge 1 commit into
mainfrom
fix/shaclgen-any-of-pattern

Conversation

@jdsika

@jdsika jdsika commented May 7, 2026

Copy link
Copy Markdown

Dependencies: None (standalone PR)

Summary

The SHACL generator (shaclgen.py) currently ignores pattern constraints specified inside any_of branches. When a slot uses any_of with a branch that has pattern: (with or without range:), 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:or validation too permissive.

Root Cause

In shaclgen.py (the for any in s.any_of: loop, ~L325–373), branch translation dispatches solely on any.range:

  • If range maps to a class → sh:class
  • If range maps to a type → sh:datatype
  • If range maps to an enum → sh:in
  • Else → empty BNode (via add_simple_data_type(func, None))

The loop never reads any.pattern from the AnonymousSlotExpression, so pattern-only branches (no range) produce empty constraint nodes, and range+pattern branches lose the pattern constraint.

Note: the top-level s.pattern check (~L394) sits in the else: arm of if s.any_of:, so it only runs when any_of is absent and cannot cover patterns inside branches.

Fix

After the if/elif/else dispatch that creates each branch BNode, add:

if any.pattern:
    g.add((range_list[-1], SH.pattern, Literal(any.pattern)))

This correctly handles all three cases:

  1. Pattern-only branch (no range): empty node gets sh:pattern[ sh:pattern "^..." ]
  2. Range + pattern: node gets both sh:datatype and sh:pattern[ sh:datatype xsd:string ; sh:pattern "^..." ]
  3. Range-only (no pattern): no-op — existing behavior preserved

This mirrors the existing top-level handling (if s.pattern: prop_pv(SH.pattern, Literal(s.pattern))), so both paths emit sh:pattern consistently.

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) in any_of
  • RangeWithPattern: range: string + pattern on same branch
  • MixedBranches: combination of range-only, pattern-only, and range+pattern branches

Two tests consume it:

  • test_any_of_with_pattern() — asserts on the generated RDF triples (which branch node carries sh:pattern, and that range-only branches do not).
  • test_any_of_with_pattern_pyshacl_end_to_end() — behavioural guard: runs pyshacl over 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, so sh:or accepts anything and the non-conforming assertions fail.

Both tests were verified to fail on main without 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:license slot uses any_of to accept:

  1. Known SPDX identifiers (enum)
  2. Arbitrary URIs (range: uri)
  3. Custom license refs matching ^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

sh:pattern is a value-node string constraint, so it is legal (and idiomatic) on the branch shapes inside sh: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).

@jdsika
jdsika force-pushed the fix/shaclgen-any-of-pattern branch 3 times, most recently from 6630206 to f9c16d5 Compare May 7, 2026 12:13
@jdsika
jdsika force-pushed the fix/shaclgen-any-of-pattern branch 6 times, most recently from d1652e5 to bfa9ea6 Compare May 12, 2026 10:53
@jdsika
jdsika force-pushed the fix/shaclgen-any-of-pattern branch from bfa9ea6 to 2c6b519 Compare June 9, 2026 15:20
@jdsika
jdsika force-pushed the fix/shaclgen-any-of-pattern branch from 2c6b519 to 09a4fbd Compare July 3, 2026 11:51
@rmessaou
rmessaou force-pushed the fix/shaclgen-any-of-pattern branch 2 times, most recently from a578620 to d736a35 Compare July 8, 2026 08:49
@jdsika jdsika self-assigned this Jul 11, 2026
@jdsika
jdsika force-pushed the fix/shaclgen-any-of-pattern branch from d736a35 to 0c334af Compare September 8, 2026 09:02
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
jdsika force-pushed the fix/shaclgen-any-of-pattern branch from 0c334af to 7394735 Compare September 11, 2026 14:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant