Skip to content

feat(generators): enforce has_member alike in SHACL and JSON Schema - #34

Open
jdsika wants to merge 1 commit into
feat/shaclgen-inlined-range-sh-nodefrom
feat/shaclgen-has-member-qualified-value-shape
Open

jdsika wants to merge 1 commit into
feat/shaclgen-inlined-range-sh-nodefrom
feat/shaclgen-has-member-qualified-value-shape

Conversation

@jdsika

@jdsika jdsika commented Oct 2, 2026 •

Copy link
Copy Markdown

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

2026-10-23, fourth push: re-stacked on the re-stacked #30 (baff32593); no conflict, no code change. test_has_member.py + test_range_expression.py + test_shaclgen.py + test_jsonschemagen.py + test_boolean_slot_compliance.py + test_validation_context.py: 4191 passed, 631 skipped, 2 xfailed.

2026-10-23, third push (adversarial review): has_member on a slot that isn't multivalued is now ignored with a warning by gen-json-schema too (it was silent; gen-shacl already warned), with test_has_member_on_a_single_valued_slot_is_ignored_with_a_warning; test_has_member_is_inherited covers slot_usage on a parent class for both generators; linkml#3495 and the pre-existing minimum_cardinality divergence recorded below. test_has_member.py: 55 passed; test_shaclgen.py + test_jsonschemagen.py + test_boolean_slot_compliance.py + test_validation_context.py + test_range_expression.py + test_has_member.py: 4146 passed, 663 skipped, 2 xfailed, 264 subtests passed.

2026-10-23, second push: rebased onto the reviewed #30 (c1527ebe7); the only conflict was the json-schema.rst paragraph both PRs extend, both sentences kept. No code change. test_shaclgen.py + test_jsonschemagen.py + test_boolean_slot_compliance.py + test_validation_context.py + test_has_member.py + test_range_expression.py: 4139 passed, 663 skipped, 2 xfailed.

2026-10-23: rebased onto the rebased #30 (38285fa89); no conflict, no code change. test_shaclgen.py + test_jsonschemagen.py + test_boolean_slot_compliance.py + test_validation_context.py: 3973 passed, 264 subtests passed. test_range_expression.py + test_has_member.py: 166 passed.

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

Stacked on #30. Covers upstream linkml#2465 (gen-shacl ignores has_member) and linkml#3495, which asks for a slot constraint that maps to JSON Schema contains: has_member is that constraint, and with this PR it holds for absent and empty slots and in class-level expressions too.

Change

has_member asks for "at least one member satisfying the condition" of a multivalued slot. Both generators now read it the same way: a member is one value, which must satisfy the value operators, range and range_expression of the expression.

slots:
  tags:
    range: string
    multivalued: true
    has_member: {equals_string: reviewed}
tags before: SHACL before: JSON Schema now: both
absent valid valid invalid
[] valid invalid invalid
[pending] valid invalid invalid
[reviewed, pending] valid valid valid
  • gen-shacl ignored has_member. It now emits sh:qualifiedValueShape with sh:qualifiedMinCount 1 (SHACL §4.7.3), on slots and in slot conditions of class-level expressions.
  • gen-json-schema applied only the member's value operators, accepted an absent slot, and ignored has_member in class-level expressions. It now emits contains on a required slot. For a slot inlined as a dict it emits not: {additionalProperties: {not: <member>}}, because contains doesn't apply to objects.
  • In a slot condition, has_member decides presence (an absent slot fails it), and the member takes the form of the condition: "definitely true" under none_of.

Decisions

Other generators

Validation

  • test_has_member.py: 48 cases. They validate the same instance with both generated artifacts (RDF through the generated JSON-LD context), plus SHACL-only and JSON-only cases. 34 fail on fix(generators): enforce range_expression on each value alike in SHACL and JSON Schema #30.
  • Out-of-tree check during development: 480 random schemas and 5,760 instances (has_member on slots and in class-level conditions, with member range_expression). gen-shacl, gen-json-schema and an independent three-valued model agreed on all of them. That harness is not part of this PR.
  • Mutation: 21 mutants of the new code, all killed; one by the doctest of states_presence.
  • Sweep: across 387 schemas in the repository, SHACL and JSON Schema output is unchanged, with identical errors and warnings.
  • Compliance: test_membership now expects SHACL to implement has_member, except on references.
  • Tests: the agreement harness of fix(generators): enforce range_expression on each value alike in SHACL and JSON Schema #30's tests moves to tests/linkml/utils/generator_agreement.py, shared with these tests.
  • CI: see the checks on the current head.

Found, not addressed here (it predates this PR): an optional multivalued slot with minimum_cardinality: 1 and no value is valid for gen-json-schema (minItems without required) and invalid for gen-shacl (sh:minCount 1), on main too. The same reasoning as for has_member would make the slot required in JSON Schema.

@jdsika jdsika self-assigned this Oct 2, 2026
@jdsika
jdsika force-pushed the feat/shaclgen-has-member-qualified-value-shape branch from 21e4983 to a0b2c05 Compare October 2, 2026 15:08
@jdsika
jdsika changed the base branch from feat/shaclgen-class-boolean-expressions to feat/shaclgen-inlined-range-sh-node October 2, 2026 15:08
@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-has-member-qualified-value-shape branch from a0b2c05 to 883509a Compare October 2, 2026 15:13
@jdsika
jdsika force-pushed the feat/shaclgen-inlined-range-sh-node branch 3 times, most recently from 937b986 to 3f14cf9 Compare October 5, 2026 09:52
@jdsika
jdsika marked this pull request as draft October 5, 2026 11:26
@jdsika jdsika changed the title feat(gen-shacl): translate has_member to sh:qualifiedValueShape feat(generators): enforce existential collection membership Oct 5, 2026
@jdsika
jdsika force-pushed the feat/shaclgen-has-member-qualified-value-shape branch from 883509a to c94f379 Compare October 5, 2026 11:47
@jdsika
jdsika marked this pull request as ready for review October 5, 2026 12:01
@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 force-pushed the feat/shaclgen-has-member-qualified-value-shape branch from c94f379 to 23553b9 Compare October 7, 2026 21:09
@jdsika jdsika changed the title feat(generators): enforce existential collection membership feat(generators): enforce has_member alike in SHACL and JSON Schema Oct 7, 2026
@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-has-member-qualified-value-shape branch from 23553b9 to a7becc8 Compare October 14, 2026 06:00
@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-has-member-qualified-value-shape branch from a7becc8 to d8a6e0d Compare October 23, 2026 08:49
@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-has-member-qualified-value-shape branch 2 times, most recently from 56d21ca to 4af1c7f Compare October 23, 2026 09:23
@jdsika
jdsika force-pushed the feat/shaclgen-inlined-range-sh-node branch from c1527eb to 19b8129 Compare October 23, 2026 10:52
has_member asks for at least one value of a multivalued slot satisfying
an expression. gen-shacl ignored it. gen-json-schema translated it as
contains, which an absent slot passes but an empty one fails, applied
only the value operators of the expression, and ignored it in
class-level expressions.

Both generators now read a member as one value, which must satisfy the
value operators, range and range_expression of the expression. A slot
without a value has no member, so has_member decides presence. In SHACL
it is an sh:qualifiedValueShape with sh:qualifiedMinCount 1; in JSON
Schema, contains on a required slot, or some value of a dict inlined by
key. In a slot condition the member takes the form of the condition.
SHACL skips a has_member it cannot express with a warning, and both
generators warn about one on a slot that is not multivalued.

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
@jdsika
jdsika force-pushed the feat/shaclgen-inlined-range-sh-node branch from 19b8129 to baff325 Compare October 23, 2026 10:58
@jdsika
jdsika force-pushed the feat/shaclgen-has-member-qualified-value-shape branch from 4af1c7f to 796fbfe 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