Skip to content

fix(shaclgen): emit sh:pattern for patterns combined with any_of - #13

Closed
jdsika wants to merge 8 commits into
mainfrom
fix/shaclgen-any-of-pattern
Closed

jdsika wants to merge 8 commits into
mainfrom
fix/shaclgen-any-of-pattern

Conversation

@jdsika

@jdsika jdsika commented May 7, 2026 •

Copy link
Copy Markdown

Mirror of linkml#3485, same head. Review there; this PR tracks the fork stack.

Summary

gen-shacl dropped pattern whenever a slot used any_of:

  • A pattern inside an any_of branch produced an empty member shape [ ]. Every value satisfies an empty shape, so sh:or accepted anything.
  • A slot-level pattern next to any_of was not emitted at all.

Both are now emitted. JSON Schema already enforces both.

license:
  any_of:
    - range: LicenseEnum
    - pattern: "^LicenseRef-[A-Za-z0-9.-]+$"
tag:
  pattern: "^[a-z]+$"
  any_of:
    - pattern: "^x"
    - pattern: "^y"

Generated SHACL (excerpt):

[ sh:path ex:license ;
  sh:or ( [ sh:in ( "MIT" "Apache-2.0" ) ] [ sh:pattern "^LicenseRef-[A-Za-z0-9.-]+$" ] ) ] .
[ sh:path ex:tag ;
  sh:pattern "^[a-z]+$" ;
  sh:or ( [ sh:pattern "^x" ] [ sh:pattern "^y" ] ) ] .

Semantics

  • In LinkML, pattern means "the string value of the slot must conform to this regular expression", and any_of "holds if at least one of the expressions hold". A slot-level pattern therefore applies to every value, whichever branch the value matches.
  • In SHACL, sh:pattern applies to each value node (SPARQL str and REGEX), and sh:or requires each value to conform to at least one member shape. A branch pattern therefore belongs on its member shape, and a slot pattern belongs on the property shape next to sh:or.

Commits

  1. Branch patterns on the sh:or member shapes. This commit is unchanged from the approved version, apart from the rebase onto main.
  2. Since approval: the slot-level sh:pattern next to sh:or. It is a separate commit so the change since approval is easy to review. Squash on merge.

Testing

tests/linkml/test_generators/input/shaclgen/any_of_pattern.yaml covers:

  • pattern-only branches
  • range and pattern on the same branch
  • mixed branches
  • a slot pattern next to any_of

The tests assert the generated triples, and check conforming and non-conforming data with pyshacl. The new case fails without commit 2.

Other generators

JSON Schema enforces both cases, and OWL emits branch patterns. Pre-existing and unchanged:

  • pydantic ignores patterns inside any_of branches.
  • ShEx emits no pattern constraints.
  • No generator resolves a structured_pattern inside an any_of branch.

@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 2 times, most recently from 0c334af to 7394735 Compare September 11, 2026 14:02
@jdsika
jdsika force-pushed the fix/shaclgen-any-of-pattern branch 2 times, most recently from 7007bcd to fdee56f Compare October 2, 2026 11:22
sagehrke and others added 3 commits October 12, 2026 16:28
Added full titles for the two October 2026 presenters.
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>
A slot that declares both `pattern` and `any_of` lost its pattern in
SHACL, because sh:pattern was only emitted when the slot had no any_of.
The pattern constrains every value of the slot independently of the
branch the value matches, so it now sits on the property shape next to
sh:or, as JSON Schema already enforces it.

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
@jdsika
jdsika force-pushed the fix/shaclgen-any-of-pattern branch from fdee56f to c684754 Compare October 13, 2026 16:00
@jdsika jdsika changed the title fix(shaclgen): emit sh:pattern for pattern constraints inside any_of fix(shaclgen): emit sh:pattern for patterns combined with any_of Oct 13, 2026
@jdsika

jdsika commented Oct 19, 2026

Copy link
Copy Markdown
Author

merged

@jdsika jdsika closed this Oct 19, 2026
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.

4 participants