Skip to content

URIs and CURIEs

A value of type uriorcurie can be a full URI, such as http://www.w3.org/2001/XMLSchema#string, or a CURIE, such as xsd:string. In a CURIE, the text before the first colon is a prefix that stands for a namespace, so a CURIE only means something together with the prefixes declared where it is written.

LinkML-Scala follows the approach discussed in linkml/linkml#3906: CURIEs are expanded when data is read, code that works with the data only sees URIs, and URIs are compacted back to CURIEs when data is written. This applies only to slots of type uriorcurie. Slots of type uri or curie are kept as they are written.

Value Read as
xsd:string, with an xsd prefix http://www.w3.org/2001/XMLSchema#string
acme:Person, with no acme prefix The URI acme:Person, with acme as its scheme, as in LinkML-Python
my_ns:Foo, with no my_ns prefix The CURIE my_ns:Foo, as my_ns can’t be a URI scheme
Person, with no colon Person after the default namespace, such as https://example.org/schema/Person
John Doe, with no colon and not a valid CURIE The text John Doe
  • A namespace is used as it is written, as in LinkML-Python. With GO: http://purl.obolibrary.org/obo/GO_, GO:0008150 is http://purl.obolibrary.org/obo/GO_0008150. So a namespace needs a separator at the end: with ex: http://example.org, ex:Person is http://example.orgPerson.
  • A value that looks like a URI is expanded too if its scheme is declared as a prefix. With http: urn:other:, http://example.org/a is urn:other://example.org/a.
  • A value with an unknown prefix is a valid URI, but most likely a mistake, and its meaning can change when it is written (see Schemas). Declare every prefix that a schema uses.
  • In a schema, validate warns about such a value with UndeclaredCuriePrefix, such as Undeclared prefix 'acme' in 'acme:Person' at /classes/Person/class_uri/. This is for a value that has the shape of a CURIE: the text before the colon is a valid prefix name, it isn’t followed by / or \, and it isn’t a scheme such as urn or mailto. With --strict, the warning makes validate fail.
  • In a schema, a value that isn’t a valid URI or CURIE, such as John Doe, is a fatal error (InvalidUriOrCurie), so the schema doesn’t load. So is a CURIE whose prefix isn’t declared and can’t be a URI scheme, such as my_ns:Foo or _:b1, as a scheme can’t have _. For example, contributors: [John Doe] fails with Invalid URI or CURIE 'John Doe' at /contributors/0/.
  • A schema built in code isn’t read, so its CURIEs aren’t expanded. In such a schema, a Curie with a declared prefix, such as Curie("ex:Person") or UriOrCurie("ex:Person"), is a fatal error (UnexpandedCurie), as generators would write it as it is. Use the URI instead, such as Uri("https://example.org/Person").
  • equals_string is a plain string, so it is checked only when the OWL generator writes it as an IRI, for a slot whose range is a URI type or a class. The generator stops with an error if the value isn’t a URI or CURIE.

A URI is written as a CURIE with the prefix of the longest namespace it starts with. That CURIE is only used if its local part is not empty, doesn’t start with // (as ex://host/a would look like a URL), and reads back as the same URI. Otherwise, the next longest namespace is tried, and if none fits, the URI is written in full. If two prefixes have the same namespace, the one with the highest precedence is used, and then the one declared first. A URI written in full that would be read back as a CURIE, such as the URI ex:Person when ex is a declared prefix, can’t be written, so writing fails with an error that names the URI and the prefix.

To write every URI in full, use the --full-uris flag of generate linkml, from owl and from ossie. In the APIs, it is the fullUris option in Scala and JavaScript, and full_uris in Python.

Each schema is read with its own prefixes, in this order of precedence:

  1. The schema’s prefixes
  2. The semweb_context prefixes, if the schema lists semweb_context in default_curi_maps
  3. The prefixes of the LinkML metamodel (see Emitted prefixes)

The prefixes of imported schemas are never used.

The default namespace, for values without a colon, is the namespace of default_prefix, or the schema’s id if there is no default_prefix (see Default default_prefix). This is not used for imports, where a value without a colon is a path relative to the importing schema.

So a CURIE in an imported schema keeps the meaning that the imported schema’s prefixes give it, even if the importing schema uses the same prefix for another namespace:

other.yaml
id: https://example.org/other
name: other
prefixes:
ex: https://other.org/
classes:
Imported:
class_uri: ex:Imported # https://other.org/Imported
root.yaml
id: https://example.org/root
name: root
prefixes:
ex: https://root.org/
imports:
- other
classes:
Local:
class_uri: ex:Local # https://root.org/Local

generate linkml writes one schema with the root’s prefixes, so it writes the class URI of Imported in full, as https://other.org/Imported. In the same way, the exact_mappings of string from linkml:types is written as http://schema.org/Text when the root schema doesn’t declare schema.

An unknown prefix breaks this. If other.yaml doesn’t declare ex, then ex:Imported is not a CURIE there. It is kept as it is, as a URI that starts with ex:, and validate warns about it. generate linkml can’t write this URI. The output declares ex as https://root.org/, so ex:Imported would be read back as https://root.org/Imported, which is a different URI. Writing it in full doesn’t help, because ex:Imported already is the full URI. So generate linkml fails with an error, also with --full-uris. To fix it, declare ex in other.yaml.

The LinkmlYamlCodec of generated Scala classes takes a PrefixMap when it decodes and encodes data. With the default, PrefixMap.empty, values are kept as they are written.

A PrefixMap has:

  • Overrides, such as the prefixes that a document declares
  • Defaults, for prefixes that are not in the overrides
  • An optional default namespace, for values without a colon
import eu.neverblink.linkml.runtime.*
import org.virtuslab.yaml.*
final case class Thing(blep: UriOrCurie) derives LinkmlYamlCodec
val prefixes = PrefixMap(defaults = Seq("ex" -> "http://example.org/"))
val codec = summon[LinkmlYamlCodec[Thing]]
val thing = codec.decode(parseYaml("blep: ex:blip").toOption.get, prefixes = prefixes)
// Thing(Uri("http://example.org/blip"))
codec.encode(thing, prefixes = prefixes).asYaml
// "blep: ex:blip\n"

The codec doesn’t read the prefixes that a document declares, such as the prefixes of a LinkML schema. To use them, read them first and pass them as overrides, as SchemaCodec does for schemas.