Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/tutorial/tutorial01/personinfo.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$defs": {
"Person": {
"additionalProperties": false,
"additionalProperties": true,
"description": "",
"properties": {
"age": {
Expand Down
10 changes: 8 additions & 2 deletions packages/linkml/src/linkml/generators/jsonschemagen.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,12 @@ def __post_init__(self):
def start_schema(self, inline: bool = False):
self.inline = inline

top_additional_properties = self.not_closed
if self.top_class:
top_class_def = self.schemaview.get_class(self.top_class)
if top_class_def is not None:
top_additional_properties = self.get_additional_properties(top_class_def)

self.top_level_schema = JsonSchema(
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
Expand All @@ -443,7 +449,7 @@ def start_schema(self, inline: bool = False):
"version": self.schema.version if self.schema.version else None,
"title": self.schema.title if self.title_from == "title" and self.schema.title else self.schema.name,
"type": "object",
"additionalProperties": self.not_closed,
"additionalProperties": top_additional_properties,
}
)

Expand Down Expand Up @@ -921,7 +927,7 @@ def get_additional_properties(self, cls: ClassDefinition) -> bool | JsonSchema:
if self.is_class_unconstrained(cls):
return True
elif not cls.extra_slots:
return False
return self.not_closed
elif cls.extra_slots.allowed is not None:
return cls.extra_slots.allowed
elif cls.extra_slots.range_expression:
Expand Down
30 changes: 28 additions & 2 deletions packages/linkml/src/linkml/generators/owlgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,11 @@ class OwlSchemaGenerator(Generator):
one direct ``is_a`` child, the generator adds
``AbstractClass rdfs:subClassOf (Child1 or Child2 or …)``, expressing the open-world covering
constraint that every instance of the abstract class must also be an instance of one of its
direct subclasses."""
direct subclasses.

.. note:: An info message is emitted when an abstract class has no children (no axiom generated).
A warning is emitted when there is only one child (covering axiom degenerates to equivalence
Parent ≡ Child). Use this flag to suppress covering axioms entirely if equivalence is undesired."""

@staticmethod
def _present(values: Iterable[_T | None]) -> list[_T]:
Expand Down Expand Up @@ -567,6 +571,26 @@ def condition_to_bnode(expr: AnonymousClassExpression) -> OWL_EXPRESSION | None:
# must be an instance of at least one of its direct subclasses.
if cls.abstract and not self.skip_abstract_class_as_unionof_subclasses:
children = sorted(sv.class_children(cls.name, imports=self.mergeimports, mixins=False, is_a=True))
if not children:
logger.info(
"Abstract class '%s' has no children. No covering axiom will be generated.",
cls.name,
)
elif len(children) == 1:
# Warn: with one child C, the covering axiom degenerates to
# Parent ⊑ C which, combined with C ⊑ Parent (from is_a),
# creates Parent ≡ C (equivalence). This is semantically
# correct per OWL 2 but may be surprising for extensible
# ontologies where more children are added later.
logger.warning(
"Abstract class '%s' has only 1 direct child ('%s'). "
"The covering axiom makes them equivalent (%s ≡ %s). "
"Use --skip-abstract-class-as-unionof-subclasses to suppress.",
cls.name,
children[0],
cls.name,
children[0],
)
if children:
child_uris = [self._class_uri(child) for child in children]
union_node = self._union_of(child_uris)
Expand Down Expand Up @@ -1716,7 +1740,9 @@ def slot_owl_type(self, slot: SlotDefinition) -> URIRef:
show_default=True,
help=(
"If true, suppress rdfs:subClassOf owl:unionOf(subclasses) covering axioms for abstract classes. "
"By default such axioms are emitted for every abstract class that has direct is_a children."
"By default such axioms are emitted for every abstract class that has direct is_a children. "
"Note: an info message is logged for abstract classes with zero children (no axiom); "
"a warning is emitted for one child (equivalence)."
),
)
@click.option(
Expand Down
Loading
Loading