Skip to content

feat(rdf): give declared annotation values and native metadata the term of their range - #58

Closed
jdsika wants to merge 3 commits into
mainfrom
feat/declared-annotation-terms
Closed

jdsika wants to merge 3 commits into
mainfrom
feat/declared-annotation-terms

Conversation

@jdsika

@jdsika jdsika commented Oct 21, 2026 •

Copy link
Copy Markdown

Mirror of linkml#4157, same head. Review there; this PR tracks the fork stack.

Read first: generators/common/annotations.py (120 lines), then owlgen.py (42); 184 lines of source in all; the rest is tests and docs.

An annotation whose tag a metaclass declares through instantiates now gets the RDF term that the slot's range gives a slot value, by the rule of linkml#4156 (linkml_runtime.utils.rdf_terms). gen-owl and gen-shacl (--include-annotations) share it through linkml.generators.common.annotations, and gen-owl applies the same rule to native metadata such as see_also.

Example

classes:
  Reviewable:
    attributes:
      reviewer: {range: nodeidentifier}
      review: {range: string}
  Person:
    instantiates: [ex:Reviewable]
    annotations: {reviewer: ex:alice, review: well defined}
main this PR
reviewer "ex:alice"@en <https://example.org/alice>
review "well defined"@en unchanged

On main the declared range is ignored: every annotation value is a string, and with --default-language every value is language-tagged, identifiers included. This is the pattern of the annotations guide; the metaclass needs no further declaration.

The rule

  • A type range gives the term of a slot value of that type (fix(rdf): give values of URI-like types one RDF term kind in every generator linkml/linkml#4156): nodeidentifier, uri and uriorcurie give IRIs, or a blank node for a _: label; string gives text, with the language; curie gives an xsd:string literal; a string-derived xsd:anyURI type gives an xsd:anyURI literal.
  • An enum range gives the permissible value's text as a plain literal.
  • A class range, a structured value or an undeclared tag keeps the generator's earlier behaviour.
  • Two metaclasses that declare the tag with different terms are an error.
  • Native metadata in gen-owl follows the metamodel slot's range: see_also, source, created_by and the mappings are IRIs, with CURIEs expanded through the schema's prefixes; license, description and title are strings; created_on is an xsd:dateTime literal.

Standards and LinkML contract

  • OWL 2 §10.1: an annotation value is an IRI, an anonymous individual or a literal, so every declared term is admissible.
  • LinkML specification, part 5: instantiates makes a metaclass's slots the declarations of an element's annotations.
  • specify IRI data type for annotations linkml/linkml#2892: cmungall, "We should implement this … declare the range in your custom annotation model". No new option and no vocabulary table: DCMI's rangeIncludes is a recommendation, and rdfs:Resource includes literals, so a property name cannot decide the term.

Behaviour changes

  • Declared annotation values become nodes or typed literals; identifiers and enum values lose the language tag.
  • Native created_on and last_updated_on become xsd:dateTime literals (were plain strings). Mapped uriorcurie metaslots stay IRIs, now by the shared rule instead of a name check.
  • Fail fast for declared values: a uri annotation value that is a relative reference or a _: label is an error.
  • Native link metadata that is neither an IRI nor a CURIE, such as a bare element name in see_also, is written as a plain literal, with a warning naming the element and metaslot. A relative IRI would make the whole graph non-standard RDF, and main writes one.
  • No snapshot changes.

Not in this PR

Testing

  • test_declared_annotations.py (44): both generators; every range kind; tag as name, CURIE or IRI; annotated classes, slots, enums, permissible values and types; inherited and imported metaclasses; blank nodes; conflicting declarations; wrong-kind negatives; native metadata; the documented DCMI example; a pyshacl check that shape annotations do not constrain data.
  • Rebased onto main d48b1976d (2026-11-04) after fix(rdf): give values of URI-like types one RDF term kind in every generator linkml/linkml#4156's squash merge; this commit is unchanged. tests/linkml 12,579 passed, tests/linkml_runtime 1,897 passed.

Fork review: #58. Refs linkml#2892, linkml#3543, linkml#3546.

@jdsika
jdsika force-pushed the feat/declared-annotation-terms branch from 4b97442 to 9020d54 Compare October 21, 2026 20:38
@jdsika
jdsika force-pushed the feat/declared-annotation-terms branch from 9020d54 to cb6a6f1 Compare October 21, 2026 20:54
@jdsika
jdsika force-pushed the fix/string-derived-anyuri-types-stay-literals branch from 373df7a to 88e231b Compare October 22, 2026 20:16
@jdsika
jdsika force-pushed the feat/declared-annotation-terms branch from cb6a6f1 to 6fe02bc Compare October 23, 2026 08:47
@jdsika
jdsika force-pushed the feat/declared-annotation-terms branch from 6fe02bc to 0fa8f65 Compare October 23, 2026 11:55
@jdsika
jdsika force-pushed the fix/string-derived-anyuri-types-stay-literals branch from 88e231b to 243442b Compare October 30, 2026 07:11
@jdsika
jdsika force-pushed the feat/declared-annotation-terms branch from 0fa8f65 to e56ed2d Compare October 30, 2026 07:27
@jdsika
jdsika changed the base branch from fix/string-derived-anyuri-types-stay-literals to main November 4, 2026 08:47
…rm of their range

An annotation whose tag a metaclass declares through instantiates gets the RDF
term that the slot's range gives a slot value (linkml_runtime.utils.rdf_terms):
nodeidentifier, uri and uriorcurie values are IRIs or blank nodes, string
values are text with the resolved language, curie values are xsd:string
literals, and an enum-ranged slot writes the permissible value's text. gen-owl
and gen-shacl share the rule through linkml.generators.common.annotations;
without a declaration both keep their earlier behaviour, and metaclasses that
declare a tag with different terms are an error.

gen-owl applies the same rule to native metadata, so see_also, source and the
mappings are IRIs by the metamodel's ranges rather than by a name check, and
created_on is an xsd:dateTime literal. A link value that is neither an IRI
nor a CURIE, such as a bare element name in see_also, is written as a plain
literal with a warning: a relative IRI would make the whole graph
non-standard RDF and push it onto the canonicalizer's rdflib fallback. OWL 2 §10.1 admits IRIs, anonymous
individuals and literals as annotation values.

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
@jdsika
jdsika force-pushed the feat/declared-annotation-terms branch from e56ed2d to 763f4fd Compare November 4, 2026 08:56
dependabot Bot and others added 2 commits November 4, 2026 10:26
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
@jdsika

jdsika commented Nov 4, 2026

Copy link
Copy Markdown
Author

merged upstream

@jdsika jdsika closed this Nov 4, 2026
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.

2 participants