Repository navigation
fix(generators): enforce range_expression on each value alike in SHACL and JSON Schema - #30
Open
jdsika wants to merge 1 commit into
Conversation
jdsika
force-pushed
the
feat/shaclgen-inlined-range-sh-node
branch
from
October 2, 2026 15:06
aee8951 to
5db175a
Compare
jdsika
changed the base branch from
main
to
feat/shaclgen-class-boolean-expressions
October 2, 2026 15:06
3 tasks done
jdsika
force-pushed
the
feat/shaclgen-inlined-range-sh-node
branch
from
October 2, 2026 15:13
5db175a to
111a453
Compare
jdsika
force-pushed
the
feat/shaclgen-inlined-range-sh-node
branch
from
October 5, 2026 07:15
111a453 to
74a2483
Compare
jdsika
marked this pull request as draft
October 5, 2026 07:28
jdsika
force-pushed
the
feat/shaclgen-inlined-range-sh-node
branch
2 times, most recently
from
October 5, 2026 09:52
937b986 to
3f14cf9
Compare
jdsika
marked this pull request as ready for review
October 5, 2026 10:06
jdsika
force-pushed
the
feat/shaclgen-class-boolean-expressions
branch
from
October 7, 2026 12:05
760eb0d to
4a9d475
Compare
jdsika
force-pushed
the
feat/shaclgen-inlined-range-sh-node
branch
2 times, most recently
from
October 7, 2026 20:03
68430f4 to
46c2ab1
Compare
jdsika
force-pushed
the
feat/shaclgen-class-boolean-expressions
branch
from
October 14, 2026 05:29
4a9d475 to
48e9234
Compare
jdsika
force-pushed
the
feat/shaclgen-inlined-range-sh-node
branch
from
October 14, 2026 05:44
46c2ab1 to
3f2a2ba
Compare
jdsika
force-pushed
the
feat/shaclgen-class-boolean-expressions
branch
from
October 23, 2026 08:49
48e9234 to
23b5c25
Compare
jdsika
force-pushed
the
feat/shaclgen-inlined-range-sh-node
branch
from
October 23, 2026 08:49
3f2a2ba to
38285fa
Compare
jdsika
force-pushed
the
feat/shaclgen-class-boolean-expressions
branch
from
October 23, 2026 09:03
23b5c25 to
78870fa
Compare
jdsika
force-pushed
the
feat/shaclgen-inlined-range-sh-node
branch
from
October 23, 2026 09:06
38285fa to
c1527eb
Compare
jdsika
force-pushed
the
feat/shaclgen-class-boolean-expressions
branch
from
October 23, 2026 10:48
78870fa to
edcb032
Compare
jdsika
force-pushed
the
feat/shaclgen-inlined-range-sh-node
branch
from
October 23, 2026 10:52
c1527eb to
19b8129
Compare
jdsika
force-pushed
the
feat/shaclgen-class-boolean-expressions
branch
from
October 23, 2026 10:56
edcb032 to
e6a40da
Compare
jdsika
force-pushed
the
feat/shaclgen-inlined-range-sh-node
branch
from
October 23, 2026 10:58
19b8129 to
baff325
Compare
3 tasks done
…L and JSON Schema
gen-shacl ignored a slot's range_expression, and gen-json-schema read it
with the two-valued builder of rules, which ignores cardinalities, the
range of a condition, and the slot_usage and aliases of the value class.
Both generators now read it as a class-level expression: each value must
satisfy it as an instance of the range class satisfies a class-level
expression. In SHACL it is an sh:node next to the range's sh:class. On a
class alternative of any_of it applies to that alternative, and in a
slot condition it takes the form of the condition. SHACL skips an
expression it cannot express with a warning, as it skips a class-level
expression.
gen-json-schema applied the value constraints of a slot inlined as a
dict to the dict instead of to each value, which only a range_expression
showed. The compact forms of such a dict, {id: value} and {id: null},
stay out of its reach.
Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
jdsika
force-pushed
the
feat/shaclgen-class-boolean-expressions
branch
from
October 30, 2026 07:56
e6a40da to
878d548
Compare
jdsika
force-pushed
the
feat/shaclgen-inlined-range-sh-node
branch
from
October 30, 2026 07:57
baff325 to
d047d2c
Compare
5 tasks
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.
Mirror of linkml#4169, same head. Review there; this PR tracks the fork stack.
Summary
A slot's
range_expressionconstrains each of its values. gen-shacl and gen-json-schema now read it the same way, as linkml#4028 reads class-level expressions: a value must satisfy the expression as an instance of the range class satisfies a class-level expression.licensemain: SHACLmain: JSON Schema{}{category: retracted}{category: draft}range_expression. It now emits ansh:nodenext to the range'ssh:class(SHACL §4.7.1), on slots, on class alternatives ofany_of, and in slot conditions, where the expression takes the form of the condition.range, readsnone_ofandexactly_one_oftwo-valued, and resolves slots globally. It now uses feat(generators): translate class-level boolean expressions alike to SHACL and JSON Schema linkml/linkml#4028's class-expression builder. This changes the generated schema for existingrange_expressionusers, as the table shows.main: for a slot inlined as a dict, the slot's own value constraints applied to the dict rather than to each value, so valid data was rejected. Fixed here, because the contract above depends on it.Decisions
equals_expressionis not translated; neither generator enforces it on slots. gen-shacl therefore logs one warning for the metamodel's ownslot_group.is_acombinations of types and enums are translated. A slot condition on such a value has no slots to constrain; both generators accept the value.Other generators
range_expressionsince Implementing range_expression in jsonschemagen linkml/linkml#2860; unchanged.range_expression; unchanged.How was this tested?
test_range_expression.py(113 cases, 2 strict xfails) validates the same instance with both generated artifacts, the RDF loaded through the generated JSON-LD context, plus SHACL-only cases.test_range_expression_nestingnow expects SHACL to implement nested range expressions.mainae3df7a45(2026-10-30), no conflict:tests/linkml12,768 passed locally; CI on the head.Areas of uncertainty
is_a: <type>/is_a: <enum>: the metamodel narrowsis_aof ananonymous_class_expressiontoclass_definition, while therange_expressioncomment envisions combining two enums, and discussion #2791 calls an expression over named ranges desirable. The compliance suite (test_range_expression_booleans,is_a: integer) and gen-json-schema already read it that way; this PR follows them. Should the metamodel widen the range?{id: value}and{id: null}escape the expression in JSON Schema, while SHACL rejects the resources they denote. Recorded as a strict xfail and in the docs.Refs linkml#1776 (cmungall: "Include range_expression in jsonschemagen").
Checklist
AI Assistance
If you used AI tools while preparing this PR, you are still the author and responsible for understanding, verifying, and defending your submission. Please engage with reviewers personally rather than through your agent during feedback and revisions. See our AI Covenant for details.
Fork review (kept from the earlier mirror description)
Stacked on #29 (upstream linkml#4028), whose three-valued reading of class-level expressions it reuses.
Change
A slot's
range_expressionconstrains each of its values. Both generators now read it the same way: a value must satisfy the expression as an instance of the range class satisfies a class-level expression.license{}{category: retracted}{category: draft}range_expression. It now emits ansh:nodenext to the range'ssh:class(SHACL §4.7.1). This applies on slots, on class alternatives ofany_of, and in slot conditions, where the expression takes the form of the condition: its "definitely true" form undersh:not.range, readsnone_ofandexactly_one_oftwo-valued, and resolves slots globally, missing the value class'sslot_usageand aliases. It now uses feat(generators): translate class-level boolean expressions alike to SHACL and JSON Schema #29's class-expression builder. This changes the generated schema for everyrange_expressionin use; the table shows the effect.main: for a slot inlined as a dict, the slot's own value constraints applied to the dict rather than to each value, so valid data was rejected.range_expressionis the only value constraint a class-valued slot carries, so nothing else showed it. Fixed here, because the contract above depends on it. The matching defect of feat(generators): translate class-level boolean expressions alike to SHACL and JSON Schema #29's class-expression builder, which demanded an array for such a slot, moved to feat(generators): translate class-level boolean expressions alike to SHACL and JSON Schema #29.Decisions
equals_expression: not translated. An earlier version translated constantequals_expressionin SHACL conditions only. Neither generator enforces it on slots, so that would have introduced a second inconsistency. It is a separate feature. (The metamodel's ownslot_groupuses it, so gen-shacl now logs one warning formeta.yaml.)is_acombinations of types and enums are translated; the metamodel describesrange_expressionas "combining existing ranges", e.g. two enums. A slot condition on such a value has no slots to constrain: SHACL skips it with a warning, and JSON Schema'spropertiesare vacuous on a scalar, so both accept the value.is_a: <type>/is_a: <enum>: the metamodel narrowsis_aof ananonymous_class_expressiontoclass_definition("A class that any instance satisfying this expression must also be an instance of",slot_usageinmeta.yaml), while itsrange_expressioncomment envisions combining two enums, and cmungall writes in discussion #2791 that the metamodel "arguably ... should allow" an expression over named ranges. The compliance suite (test_range_expression_booleans,is_a: integer) and gen-json-schema already read it that way, so both generators keep doing so; the metamodel gap is not resolved here.imports_closure()call: no case needed it; the imported-slot test passes without it.Other generators
transform_class_expressionhas emitted open-world restrictions forrange_expressionsince Implementing range_expression in jsonschemagen linkml/linkml#2860; unchanged.range_expressionsupport; unchanged.Limitations
{id: value}and{id: null}of a slot inlined as a dict are not constrained by the expression; SHACL rejects the resources they denote. Recorded bytest_range_expression_on_compact_dict_forms(strict xfail) and in the generator docs.Validation
test_range_expression.py: 113 cases plus the 2 xfails. They validate the same instance with both generated artifacts (RDF through the generated JSON-LD context), plus SHACL-only cases; 32 failed on the previous head of this PR and 71 on feat(generators): translate class-level boolean expressions alike to SHACL and JSON Schema #29.any_ofalternatives, class-level conditions). gen-shacl, gen-json-schema and an independent three-valued model agreed on all of them; the previous head had 3,796 disagreements. That harness is not part of this PR.rangeversus slotrangein JSON Schema) is equivalent there; its SHACL counterpart is killed.slot_group, which usesequals_expression.test_range_expression_nestingnow expects SHACL to implement nested range expressions (it fails on feat(generators): translate class-level boolean expressions alike to SHACL and JSON Schema #29).Found, not addressed here (they predate this PR):
value_presenceinside aslot_usagerange_expression.