Repository navigation
Conversation
jdsika
force-pushed
the
feat/shaclgen-class-boolean-expressions
branch
3 times, most recently
from
September 24, 2026 12:04
022e25c to
3904d7d
Compare
8 of 18 tasks
jdsika
force-pushed
the
feat/shaclgen-class-boolean-expressions
branch
2 times, most recently
from
October 2, 2026 11:22
d90a2ab to
760eb0d
Compare
This was referenced Oct 2, 2026
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-class-boolean-expressions
branch
2 times, most recently
from
October 14, 2026 05:29
4a9d475 to
48e9234
Compare
jdsika
force-pushed
the
feat/shaclgen-class-boolean-expressions
branch
3 times, most recently
from
October 23, 2026 10:48
78870fa to
edcb032
Compare
…SHACL and JSON Schema gen-shacl dropped class-level any_of, all_of, exactly_one_of and none_of, so a class stating "a code or a name is required" generated shapes that accepted everything. gen-json-schema translated them, but only for the declaring class, and read an absent slot differently depending on where an expression was nested. Both generators now give class expressions one semantics, shared in linkml.generators.common.class_expression: - A class expression constrains every instance of its class, so a class carries the expressions of its ancestors and mixins, translated in its own context. - The specification does not say what a slot condition means for an absent slot. As an SQL CHECK constraint (ISO/IEC 9075) reads a condition on null, a condition that doesn't decide whether its slot may be absent is unknown for an absent slot. An instance is invalid only when an expression is definitely false, and exactly_one_of holds when exactly one member is definitely true. The operators compose, and gen-json-schema no longer rejects an instance meeting only the first member of an exactly_one_of while accepting one meeting none. gen-shacl emits sh:or, sh:and, sh:xone and sh:not (the operators' exact_mappings, SHACL 4.6) over anonymous member shapes, each in its "not false" form or, under sh:not, its "definitely true" form, where such a condition also has sh:minCount 1. Conditions translate required, value_presence, the cardinalities, minimum/maximum_value, pattern, equals_string(_in), equals_number and range; a parameter SHACL allows once per shape that one condition needs twice moves into an sh:and member. An operator whose members use anything else is skipped whole, with one warning naming the shapes it is missing from. gen-json-schema builds the same forms with anyOf, allOf, oneOf and not, resolves conditions on the induced slot, and also enforces cardinalities (minItems / maxItems, or minProperties / maxProperties for a slot inlined as a dict, whose values are the values of the dict) and range in class-level conditions. value_presence: ABSENT on several slots of one expression now requires each to be absent rather than not all of them present, in rules too. An is_a member names a class that every instance of the class carrying the expression belongs to (the class or an ancestor), that only some belong to (through a descendant), or that none belongs to, since an instance belongs to its class and that class's ancestors. Only the second case checks the instance: sh:class in SHACL, and in JSON Schema, which carries no type, a reference to the class's definition. Before, a class inherited its parent's any_of over its own subclasses as sh:class and $ref to itself, which recursed without end in JSON Schema and, in SHACL, rejected a node typed only as a subclass. The slot loop's range dispatch and sh:path computation move into _add_range and _slot_iri; output for schemas without class expressions is unchanged. The compliance tests test_class_any_of and test_class_any_of_with_required run for SHACL. Signed-off-by: jdsika <[email protected]>
jdsika
force-pushed
the
feat/shaclgen-class-boolean-expressions
branch
from
October 23, 2026 10:56
edcb032 to
e6a40da
Compare
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#4028, same head. Review there; this PR tracks the fork stack.
Summary
Class-level
any_of,all_of,exactly_one_ofandnone_ofnow mean the same in gen-shacl and gen-json-schema.This becomes
sh:or ( [sh:property [sh:path ex:code ; sh:minCount 1]] … )and JSON SchemaanyOf, on this class and on every subclass.Semantics
The semantics live in one shared module,
linkml.generators.common.class_expression, so the generators can't drift apart.Inheritance: a class carries the expressions of its ancestors and mixins, translated in its own context, so
slot_usageapplies.Absent slots: the spec doesn't define what a slot condition means for an absent slot (Specification: define what a slot condition means for an absent slot in class-level boolean expressions #49). This PR reads it as an SQL CHECK constraint reads null (ISO/IEC 9075 §4.10.2: "satisfied if and only if the specified search condition is not false"):
The deciding conditions are
value_presence: PRESENT/ABSENT,required: true, a minimum or exact cardinality ≥ 1, and a maximum or exact cardinality of 0.exactly_one_of: holds when exactly one member is definitely true, whatever the others are. This is whatsh:xoneandoneOfcompute over the members' "definitely true" forms. It is stricter than a Kleene reading, which would leave{}and an instance meeting no member unknown, and so valid; the compliance suite'sexactly_one_ofcases decide neither way, because their members userequired.{}{label: A}{label: B}any_of: [label = A]none_of: [label = A]exactly_one_of: [label = A, note = B]Why this reading:
all_of: [none_of: [X]]meansnone_of: [X], andnone_of: [none_of: [X]]meansX.exactly_one_of, which rejected{label: A}and accepted{label: B}.The vacuous reading (JSON Schema
properties, SHACL Core) and the existential one (XPath, ShEx) each contradict the compliance suite.Rules keep their closed-world reading (linkml#3989, linkml#3990).
Translation
sh:or/sh:and/sh:xone/sh:notover anonymous member shapes. These are the operators'exact_mappings(SHACL §4.6).sh:not, in its "definitely true" form, where a non-deciding condition addssh:minCount 1.is_amember names a class every instance of the shape's class belongs to (nothing is emitted), one that only some belong to through a descendant (sh:class, orsh:nodewith native names), or one that none belongs to (a shape nothing conforms to). An instance belongs to its class and that class's ancestors, byis_aand bymixins; a class that may also be another is declared as a subclass with that mixin.exactly_one_ofbecomessh:xoneover the "definitely true" member forms.sh:and(§2.1.1).anyOf/allOf/oneOf/not;is_amember the same way; where only some instances belong to the class, the instance must match that class's definition, since a JSON object carries no type.minItems/maxItems, orminProperties/maxPropertiesfor a slot inlined as a dict, whose values are the values of the dict) andrangein class-level conditions.value_presence: ABSENTon several slots requires each to be absent, rules included (fixes gen-json-schema: multiplevalue_presence: ABSENTpostconditions in one rule compile to "not both present" instead of "neither present" linkml/linkml#3719).has_memberor a slot-level boolean inside a condition): gen-shacl skips the operator whole, with one warning naming the affected shapes.Testing
test_shaclgen.py:slot_usage, sharedclass_uri, warnings;test_jsonschemagen.py: inheritance, severalABSENTslots, conditions on induced slots.test_class_any_of,test_class_any_of_with_requiredandtest_class_boolean_with_expressionsnow run for SHACL; the last skips the schemas whose harness data writes a reference as an untyped node, which the slot'ssh:classrejects.Behaviour changes in gen-json-schema
exactly_one_ofwith absent slots, follow the table above.range.owlgen is unchanged. OWL is open-world and can't express closed-world presence.
Closes #48 (class expressions not applied to subclasses in gen-json-schema).
Follow-ups: #49 (spec: absent-slot semantics) and #50 (spec: two-operand
none_ofis NOR, not XOR).Fork stack: #29 → #30 → #34.