Skip to content

fix(rdf): give values of URI-like types one RDF term kind in every generator - #32

Open
jdsika wants to merge 1 commit into
mainfrom
fix/string-derived-anyuri-types-stay-literals
Open

jdsika wants to merge 1 commit into
mainfrom
fix/string-derived-anyuri-types-stay-literals

Conversation

@jdsika

@jdsika jdsika commented Oct 2, 2026 •

Copy link
Copy Markdown

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

One rule decides whether a value of a type becomes an IRI, a blank node or a literal in RDF. linkml_runtime.utils.rdf_terms.type_term is shared by gen-jsonld-context, gen-owl, gen-shacl, gen-shex, gen-yarrrml and the RDF dumper and loader.

Example

types:
  FilePath:
    typeof: string
    uri: xsd:anyURI
slots:
  homepage: {range: uri}        # a link
  file_path: {range: FilePath}  # a URI reference held as text, may be relative

For homepage: https://alice.example.org/ and file_path: ./docs/cv.pdf:

main this PR
gen-jsonld-context homepage → "…"^^xsd:anyURI literal, file_path → same literal homepage → @id, file_path → xsd:anyURI literal
RDF dumper homepage → IRI, file_path → <./docs/cv.pdf> (not an IRI, RDF 1.1 §3.2) homepage → IRI, file_path → "./docs/cv.pdf"^^xsd:anyURI
gen-shacl both sh:nodeKind sh:IRI sh:IRI / sh:datatype xsd:anyURI
gen-owl both owl:DatatypeProperty owl:ObjectProperty / owl:DatatypeProperty
RDF loader (strict) rejects the dumper's own IRI for homepage round trip

So on main, data written through the generated context fails the generated shapes, and the dumper writes relative IRIs. The --xsd-anyuri-as-iri option only moved the problem: with it, ./docs/cv.pdf is resolved against the reader's location.

The rule

Type RDF term
uri, uriorcurie and types derived from them IRI (CURIEs expanded)
objectidentifier (shex:iri) IRI
nodeidentifier (shex:nonLiteral), types with uri: rdfs:Resource IRI or blank node
every other type, e.g. typeof: string with uri: xsd:anyURI literal of its datatype, as written

The rule reads the inherited, expanded datatype, so its spelling does not matter, and it follows the typeof chain, as gen-python does for the Python base class (ImageLink(URI) vs FilePath(str)). uri and uriorcurie are recognised by name and definition, not by from_schema, which merging imports rewrites. An explicit datatype on a derived type wins (typeof: uri with uri: xsd:string is a string literal).

Standards

  • RDF 1.1 Concepts §3.1: IRIs, literals and blank nodes are distinct; §3.2: "IRIs in the RDF abstract syntax MUST be absolute".
  • XSD 1.1 §3.3.17: "An anyURI value can be absolute or relative". A URI reference held as text is an xsd:anyURI literal, not an IRI.
  • OWL 2 §5.8.1: a property is an object or a data property, so a slot needs one term kind, not one per value.
  • SHACL §4.1.3 node kinds; §4.8.3 sh:in compares terms exactly; ShEx 2.1 §5.4.2 iri, bnode, nonliteral.

LinkML contract and history

Behaviour changes

  • gen-jsonld-context, gen-owl: uri/uriorcurie slots and derived types become @id and owl:ObjectProperty by default (before: with the option; OWL also had owl:allValuesFrom xsd:string). The metamodel and Biolink context snapshots change accordingly; OWL type_objects mode is unchanged.
  • gen-shacl, gen-shex, dumper: a string-derived xsd:anyURI type is a literal (before: IRI).
  • Node types: objectidentifier is sh:IRI (was sh:BlankNodeOrIRI); nodeidentifier and rdfs:Resource types are IRIs or blank nodes in the context, dumper, OWL, ShEx and YARRRML (before: ^^shex:nonLiteral literals that failed SHACL and reloaded as None). Closes nodeidentifier values are written as shex:nonLiteral literals by the JSON-LD context and rdflib_dumper, so they never satisfy gen-shacl #51.
  • gen-shacl: sh:in values and sh:defaultValue take the slot's term kind; a curie slot with subproperty_of was unsatisfiable.
  • Dumper: a typeof: uri type without its own uri was a plain string literal; now the inherited datatype applies. Relative values of IRI types raise ValueError.
  • Loader: accepts IRIs for node-kind ranges in strict mode (cast_literals=False) and returns _:label for blank nodes in IRI-or-blank-node slots.
  • Fail fast: a type datatype with an undeclared prefix is an error (xsd, shex, rdfs are known).
  • --xsd-anyuri-as-iri: deprecated in 1.13.0, no effect, removal scheduled for 1.14.0. To keep a uri slot as text, give it a string-derived xsd:anyURI type. SEMICeu/core_person_linkml's three range: uri slots would need this.

Alternatives not chosen

Not in this PR

  • curie stays an unexpanded xsd:string literal although types.yaml says it MUST be expanded in RDF; JSON-LD cannot expand inside a literal, so it needs a decision (Using wrong XSD type for URIorCURIE linkml/linkml#2213).
  • Validation: JSON Schema has format: uri only when the induced base is URI; the runtime URI class accepts relative references; the loader compacts IRIs to CURIEs also for uri slots.
  • Runtime Identifier.is_valid rejects blank-node labels such as _:b1, so nodeidentifier blank nodes work through the context and the loader only with rdfs:Resource types until that is fixed.
  • linkml-model: metamodel context regeneration, spec and types.yaml text.
  • Release: linkml needs linkml-runtime of the same release for the new module (floor bump at release, as for earlier runtime additions).

Testing

  • test_uri_term_kinds.py (61): context, SHACL, OWL, ShEx and YARRRML output; dumper and context data paths validated with pyshacl; loader round trip and blank nodes; wrong-kind negatives, relative links, curie with subproperty_of. 31 fail on main.
  • test_rdf_terms.py (35): datatypes, spelling, identity in own and merged schemas, prefixes, values.

@jdsika jdsika self-assigned this Oct 2, 2026
@jdsika
jdsika marked this pull request as draft October 5, 2026 10:27
@jdsika jdsika changed the title fix(rdf): keep string-derived xsd:anyURI types as typed literals fix(rdf): preserve string-derived anyURI literals across generators Oct 5, 2026
@jdsika
jdsika force-pushed the fix/string-derived-anyuri-types-stay-literals branch from 75f9be6 to 3e1b4ef Compare October 5, 2026 10:42
@jdsika
jdsika marked this pull request as ready for review October 5, 2026 10:55
@jdsika
jdsika force-pushed the fix/string-derived-anyuri-types-stay-literals branch from 3e1b4ef to d9d2745 Compare October 14, 2026 06:15
@jdsika
jdsika force-pushed the fix/string-derived-anyuri-types-stay-literals branch from d9d2745 to 419b047 Compare October 21, 2026 15:56
@jdsika jdsika changed the title fix(rdf): preserve string-derived anyURI literals across generators fix(rdf): give values of URI-like types one RDF term kind in every generator Oct 21, 2026
@jdsika
jdsika force-pushed the fix/string-derived-anyuri-types-stay-literals branch 2 times, most recently from a378558 to 373df7a Compare October 21, 2026 19:25
…nerator

A value of a type is an IRI, a blank node or a literal according to one rule,
linkml_runtime.utils.rdf_terms.type_term, which gen-jsonld-context, gen-owl,
gen-shacl, gen-shex, gen-yarrrml and the RDF dumper and loader now share:

- uri, uriorcurie and the types derived from them are IRIs;
- objectidentifier is an IRI, and nodeidentifier and types with
  uri: rdfs:Resource are IRIs or blank nodes;
- every other type is a literal of its datatype, including a type derived
  from string with uri: xsd:anyURI, which keeps URI references as text.

The rule resolves the inherited, expanded datatype, so its spelling does not
matter, and recognises uri and uriorcurie by name and definition rather than
by from_schema, which merging imports rewrites. A datatype with an undeclared
prefix is an error (xsd, shex and rdfs are known). The dumper writes the
inherited datatype of a derived type and rejects relative values of IRI types
(RDF 1.1 Concepts §3.2); the loader accepts IRIs for node-kind ranges in
strict mode, keeps uri values complete, compacts uriorcurie values to CURIEs
and keeps blank-node labels. gen-shacl gives sh:in values and sh:defaultValue
the slot's term kind.

The --xsd-anyuri-as-iri option of gen-owl and gen-jsonld-context is
deprecated in 1.13.0, has no effect, and is scheduled for removal in 1.14.0.

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
@jdsika
jdsika force-pushed the fix/string-derived-anyuri-types-stay-literals branch from 373df7a to 88e231b Compare October 22, 2026 20:16
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.

nodeidentifier values are written as shex:nonLiteral literals by the JSON-LD context and rdflib_dumper, so they never satisfy gen-shacl

1 participant