Skip to content

fix(generators): enforce range_expression on each value alike in SHACL and JSON Schema - #30

Open
jdsika wants to merge 1 commit into
feat/shaclgen-class-boolean-expressionsfrom
feat/shaclgen-inlined-range-sh-node
Open

jdsika wants to merge 1 commit into
feat/shaclgen-class-boolean-expressionsfrom
feat/shaclgen-inlined-range-sh-node

Conversation

@jdsika

@jdsika jdsika commented Oct 2, 2026 •

Copy link
Copy Markdown

Mirror of linkml#4169, same head. Review there; this PR tracks the fork stack and keeps the fork review below.

2026-10-23, third push (corrected): re-stacked on the reviewed #29 (e6a40da1c), whose is_a membership rule this PR's _class_expression_shape now combines with its own type-and-enum branch: a class member goes through membership, a type or enum member gives its value constraint, and a value that is no class instance keeps sh:class / the structural $ref. The first re-stack (19b8129e3) made the compliance case test_range_expression_booleans fail, because a linkml:Any value class was read as able to be no class; #29 now leaves linkml:Any undetermined. test_shaclgen.py + test_jsonschemagen.py + test_boolean_slot_compliance.py + test_validation_context.py + test_range_expression.py: 4135 passed, 631 skipped, 2 xfailed.

2026-10-23, second push (adversarial review): rebased onto the rebased #29 (78870fa95), which now carries the dict-inlining fix of its class-expression builder that this PR had bundled (a condition on a slot inlined as a dict demanded an array and rejected valid data); the fix of the slot's own value constraints stays here, where range_expression is the only constraint that shows it. Review changes: the metamodel facts on is_a under Decisions, the compact dict forms recorded under Limitations with a strict xfail, the dict test reduced to its range_expression case. test_range_expression.py: 113 passed, 2 xfailed; test_shaclgen.py + test_jsonschemagen.py + test_boolean_slot_compliance.py + test_validation_context.py: 3977 passed, 663 skipped, 261 subtests passed.

2026-10-23: rebased onto the rebased #29. Conflicts with linkml#3485 (34b78f497): the any_of branch sh:pattern propagation now goes through this PR's branch_pv, and the slot-level sh:pattern block sits next to the range_expression block. No behaviour change.

2026-10-14: rebased onto the rebased #29; no code change.

Stacked on #29 (upstream linkml#4028), whose three-valued reading of class-level expressions it reuses.

Change

A slot's range_expression constrains 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.

Document:
  slot_usage:
    license:
      range_expression:
        none_of:
          - slot_conditions: {category: {equals_string: retracted}}
license before: SHACL before: JSON Schema now: both
{} valid invalid valid
{category: retracted} valid invalid invalid
{category: draft} valid valid valid

Decisions

  • Untranslatable input: an expression SHACL can't express is skipped with one warning naming the slot, the reason and the class shapes, as feat(generators): translate class-level boolean expressions alike to SHACL and JSON Schema #29 does for class-level expressions. An earlier version raised instead, which contradicted that policy.
  • equals_expression: not translated. An earlier version translated constant equals_expression in 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 own slot_group uses it, so gen-shacl now logs one warning for meta.yaml.)
  • Non-class ranges: is_a combinations of types and enums are translated; the metamodel describes range_expression as "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's properties are vacuous on a scalar, so both accept the value.
  • is_a: <type> / is_a: <enum>: the metamodel narrows is_a of an anonymous_class_expression to class_definition ("A class that any instance satisfying this expression must also be an instance of", slot_usage in meta.yaml), while its range_expression comment 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.
  • Dropped imports_closure() call: no case needed it; the imported-slot test passes without it.

Other generators

  • gen-owl: transform_class_expression has emitted open-world restrictions for range_expression since Implementing range_expression in jsonschemagen linkml/linkml#2860; unchanged.
  • References: SHACL checks the referenced node's description in the data graph, while JSON Schema sees only the identifier. This is a limit of the format.
  • ShEx, Python, Pydantic, SQL: no range_expression support; unchanged.

Limitations

  • Compact dict forms in JSON Schema: the forms {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 by test_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.
  • Out-of-tree check during development: 960 random schemas and 26,654 instances (slots, lists, any_of alternatives, 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.
  • Mutation: 22 mutants of the new code; 21 killed. The survivor (condition range versus slot range in JSON Schema) is equivalent there; its SHACL counterpart is killed.
  • Sweep: across 387 schemas in the repository, SHACL and JSON Schema output is unchanged, with identical errors. The one new warning is for the metamodel's own slot_group, which uses equals_expression.
  • Compliance: test_range_expression_nesting now expects SHACL to implement nested range expressions (it fails on feat(generators): translate class-level boolean expressions alike to SHACL and JSON Schema #29).
  • CI: see the checks on the current head.

Found, not addressed here (they predate this PR):

  • gen-jsonld-context maps an aliased slot to the alias IRI, while gen-shacl and gen-owl use the slot IRI.
  • SchemaLoader, and so gen-jsonld-context, crashes on value_presence inside a slot_usage range_expression.

@jdsika jdsika self-assigned this Oct 2, 2026
@jdsika
jdsika force-pushed the feat/shaclgen-inlined-range-sh-node branch from aee8951 to 5db175a Compare October 2, 2026 15:06
@jdsika
jdsika changed the base branch from main to feat/shaclgen-class-boolean-expressions October 2, 2026 15:06
@jdsika
jdsika force-pushed the feat/shaclgen-inlined-range-sh-node branch from 5db175a to 111a453 Compare October 2, 2026 15:13
@jdsika
jdsika force-pushed the feat/shaclgen-inlined-range-sh-node branch from 111a453 to 74a2483 Compare October 5, 2026 07:15
@jdsika
jdsika marked this pull request as draft October 5, 2026 07:28
@jdsika jdsika changed the title feat(gen-shacl): validate inlined values by their range shape with --inlined-as-node fix(gen-shacl): enforce contextual class range expressions Oct 5, 2026
@jdsika
jdsika force-pushed the feat/shaclgen-inlined-range-sh-node branch 2 times, most recently from 937b986 to 3f14cf9 Compare October 5, 2026 09:52
@jdsika
jdsika marked this pull request as ready for review October 5, 2026 10:06
@jdsika
jdsika force-pushed the feat/shaclgen-class-boolean-expressions branch from 760eb0d to 4a9d475 Compare October 7, 2026 12:05
@jdsika
jdsika force-pushed the feat/shaclgen-inlined-range-sh-node branch 2 times, most recently from 68430f4 to 46c2ab1 Compare October 7, 2026 20:03
@jdsika jdsika changed the title fix(gen-shacl): enforce contextual class range expressions fix(generators): enforce range_expression on each value alike in SHACL and JSON Schema Oct 7, 2026
@jdsika
jdsika force-pushed the feat/shaclgen-class-boolean-expressions branch from 4a9d475 to 48e9234 Compare October 14, 2026 05:29
@jdsika
jdsika force-pushed the feat/shaclgen-inlined-range-sh-node branch from 46c2ab1 to 3f2a2ba Compare October 14, 2026 05:44
@jdsika
jdsika force-pushed the feat/shaclgen-class-boolean-expressions branch from 48e9234 to 23b5c25 Compare October 23, 2026 08:49
@jdsika
jdsika force-pushed the feat/shaclgen-inlined-range-sh-node branch from 3f2a2ba to 38285fa Compare October 23, 2026 08:49
@jdsika
jdsika force-pushed the feat/shaclgen-class-boolean-expressions branch from 23b5c25 to 78870fa Compare October 23, 2026 09:03
@jdsika
jdsika force-pushed the feat/shaclgen-inlined-range-sh-node branch from 38285fa to c1527eb Compare October 23, 2026 09:06
@jdsika
jdsika force-pushed the feat/shaclgen-class-boolean-expressions branch from 78870fa to edcb032 Compare October 23, 2026 10:48
@jdsika
jdsika force-pushed the feat/shaclgen-inlined-range-sh-node branch from c1527eb to 19b8129 Compare October 23, 2026 10:52
…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 <[email protected]>
@jdsika
jdsika force-pushed the feat/shaclgen-class-boolean-expressions branch from edcb032 to e6a40da Compare October 23, 2026 10:56
@jdsika
jdsika force-pushed the feat/shaclgen-inlined-range-sh-node branch from 19b8129 to baff325 Compare October 23, 2026 10:58
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