Implementation differences
Feature comparison with LinkML-Python
Section titled “Feature comparison with LinkML-Python”The following features are not yet supported or are partially supported in LinkML-Scala:
- Arrays
- Boolean expressions (
any_of,all_of,exactly_one_of,none_of) in the JSON Schema, OWL and Pydantic generators- JSON Schema and Pydantic ignore them, OWL supports only
any_ofranges
- JSON Schema and Pydantic ignore them, OWL supports only
- Partial support for default values (
ifabsent)- Only enum and boolean defaults are supported, only in the Scala generator
- Partial support for computed values (e.g.,
equals_expression)- Only string interpolation is supported, only in the Scala generator
- Partial support for type designators (
designates_type)- Supported in the Scala, TypeScript, Pydantic and JSON Schema generators and in YAML/JSON serialization. Not yet in SHACL.
- Enum inheritance, dynamic enums (
include,minus,reachable_from) - Rules (
rules)
These features are supported in LinkML-Scala but not in LinkML-Python (or are supported in LinkML-Python to a lesser degree):
- Support for extra data (
extra_slots) in more generators- A
range_expressionallows any extra data, which is not checked against it (see below) - Extra data is only validated, not kept: decoding drops it, so it is lost on a round trip (see below)
- A
- Support for
union_of- JSON Schema, Scala, TypeScript, and Pydantic generators support type unions.
- Some constraints on unioned types may be lost, depending on the expressiveness of the target language.
- Support for language strings
- Better support for imports
- The
.yamland.ymlextensions are optional and interchangeable, and each imported schema is loaded only once
- The
- Support for
all_of,exactly_one_of,none_of, nested boolean expressions and cardinality inside them in the SHACL generator (see Boolean expressions in SHACL) - IRIs with non-ASCII characters are accepted in slots of type
urianduriorcurie.
These features work differently in LinkML-Scala than in the LinkML specification and LinkML-Python:
- Inherited boolean expressions: a subclass accepts only values that its parent classes and mixins accept
These features follow the LinkML specification, while LinkML-Python works differently:
- Boolean metaslots in
slot_usage: aslot_usagecan turnrequiredormultivaluedon, but not off
Boolean expressions in SHACL
Section titled “Boolean expressions in SHACL”In boolean expressions, the SHACL generator of LinkML-Python 1.12.1 checks only the range of any_of members.
LinkML-Scala also checks all_of, exactly_one_of, none_of, and the patterns, value bounds, cardinality and nested boolean expressions of members.
Inherited boolean expressions
Section titled “Inherited boolean expressions”The LinkML specification joins the any_of, all_of, exactly_one_of and none_of lists that a class inherits from its parent classes, mixins and the slot definition.
LinkML-Python 1.12.1 keeps only the most specific list.
LinkML-Scala joins the all_of and none_of lists, and for any_of and exactly_one_of keeps the most specific list and requires the inherited lists too.
So a value of a class is also a valid value of each of its parent classes and mixins.
See Boolean expressions for details and examples.
Boolean metaslots in slot_usage
Section titled “Boolean metaslots in slot_usage”The LinkML specification combines boolean metaslots, like required and multivalued, with OR when it derives a slot.
LinkML-Scala does the same. LinkML-Python 1.12.1 uses the value from the closest slot_usage instead.
So a slot_usage can make a slot required or multivalued, but not optional or single-valued when the slot or a parent class already sets it.
See Boolean metaslots for details and examples.
Imports
Section titled “Imports”Imports are easier to write in LinkML-Scala: file extensions are optional and interchangeable, and each imported schema is loaded only once.
| Case | LinkML-Python 1.12.1 | LinkML-Scala |
|---|---|---|
imports: [core] with only core.yml on disk |
Fails | Reads core.yml |
imports: [core.yaml] or [core.yml] |
Fails: reads core.yaml.yaml or core.yml.yaml |
Works |
One file imported as dir/a, dir/./a, dir//a or dir/x/../a |
Loaded once per spelling | Loaded once |
| One file reached through a symbolic or hard link | Loaded twice | Loaded once (JVM and Native) |
| An import back to the root schema | Loads the root again, unless its name matches the file name |
Loaded once, when the root is loaded from a path |
A CURIE import, such as ex:core |
Expanded with the prefixes of all schemas loaded so far; when two schemas declare ex differently, the result depends on the load order |
Expanded with the prefixes of the schema that contains the import |
Two files with the same id and different content |
Both loaded | Import error that names both files |
Both support import maps, which load an import from another location (see Import maps). They differ in a few ways:
| Case | LinkML-Python 1.12.1 | LinkML-Scala |
|---|---|---|
A key that is a CURIE, such as units:core |
Matches the import units:core, but not the import https://example.org/units/core |
Rejected. Write the full URI, which matches both imports |
| A relative location | Joined to the directory of the importing schema | Used as it is: relative to the current directory, or a key of the in-memory map |
Eager validation of references
Section titled “Eager validation of references”All LinkML references (like slot_name in slots: [ slot_name ]) are eagerly checked when creating the SchemaView.
SchemaView is not able to proceed with derivation if this requirement is not satisfied.
For the same reason, loops are rejected when creating the SchemaView: a class or slot that inherits from itself through is_a or mixins, a type that is its own typeof ancestor, and a type that is a member of its own union_of (directly, through nested unions, or through the typeof parent of a member).
Emitted prefixes
Section titled “Emitted prefixes”Metamodel emit_prefixes is honored, meaning the following prefixes are defined automatically in all schemas:
linkml: https://w3id.org/linkml/rdf: http://www.w3.org/1999/02/22-rdf-syntax-ns#rdfs: http://www.w3.org/2000/01/rdf-schema#xsd: http://www.w3.org/2001/XMLSchema#skos: http://www.w3.org/2004/02/skos/core#dcterms: http://purl.org/dc/terms/OIO: http://www.geneontology.org/formats/oboInOwl#owl: http://www.w3.org/2002/07/owl#pav: http://purl.org/pav/
CURIEs
Section titled “CURIEs”CURIEs in uriorcurie slots are expanded to URIs when a schema is loaded, with the prefixes of the schema they are written in, and URIs are compacted to CURIEs when a schema is written.
So generate linkml writes a URI from an imported schema in full when the output schema has no prefix for it, and a CURIE never changes its meaning when it moves into another schema.
See URIs and CURIEs.
Default default_range
Section titled “Default default_range”If a default_range is not provided, then it will be filled with string (as per the specification)
This is always honored in LinkML-Scala.
To make generators to emit an “accept anything” schema, set the range to a class with uri linkml:Any, as described here.
Default default_prefix
Section titled “Default default_prefix”If a default_prefix is not provided, then the schema’s id will be used instead.
This ensures that all schema Elements always can construct a valid URI, even if it is synthetic.
Always meaningful enums
Section titled “Always meaningful enums”Enum permissible_values always have a meaning, even if meaning is not explicitly defined.
Thanks to this, enum values can always be represented as IRIs in RDF, and LinkML-Scala does not allow string-based enums.
Additional identifier constraints
Section titled “Additional identifier constraints”The identifier slot for classes must have a scalar type range.
It is an error to have an enum or class identifier.
Extra data
Section titled “Extra data”As in the metamodel, instances of a class may only have data for its slots, unless the class allows more with extra_slots:
classes: Person: extra_slots: allowed: true attributes: name: {}LinkML-Scala honors extra_slots when decoding with generated Scala classes, as well as in the JSON Schema (additionalProperties), SHACL (sh:closed), TypeScript ([key: string]: unknown) and Pydantic (extra="allow") generators.
LinkML-Python 1.12.1 honors extra_slots in its JSON Schema, Java and Go generators.
allowed: trueallows any extra data.allowed: false, or noextra_slots, forbids it. An explicitallowed: falsehas precedence over arange_expression.- A
range_expressionalone allows extra data matching it. LinkML-Scala allows any extra data then, without checking it against the expression, while the JSON Schema of LinkML-Python checks extra data against it. extra_slotsis not inherited: a subclass of a class allowing extra data forbids it, unless its ownextra_slotsallows it too.
The --open option of the JSON Schema, SHACL, TypeScript and Pydantic generators allows extra data in all classes.
In LinkML-Scala, extra_slots only controls validation. When decoding, allowed extra data is accepted and then discarded: generated Scala classes have a fixed set of fields, so there is nowhere to store it, and encoding the object again will not write it back. LinkML-Python can attach unknown attributes to an object at runtime, which Scala classes cannot do.
Tree root extension
Section titled “Tree root extension”LinkML-Scala provides a tree_root_as extension for classes, which allows specifying how the tree_root class will be laid out.
For example, this allows specifying that the root of a JSON document should be a JSON array with instances of this class:
SomeClass: tree_root: true extensions: tree_root_as: list attributes: id: range: string identifier: true value: range: integer required: trueJSON Schema generated from this LinkML schema accepts:
[ { "id": "1", "value": 1 }, { "id": "2", "value": 2 }, { "id": "3", "value": 3 }]But rejects:
{ "id": "1", "value": 1 }tree_root_as accepts the following options:
plain: object instance,optional: object instance or null,list: array of object instances, may be empty,compact_dict: dict object mapping the key field to the identifier-optional object instancessimple_dict: dict object mapping the key field to the value field of the object instances
LinkML-Python doesn’t know this extension, so its generators always expect one object. In LinkML-Scala, the JSON Schema and Pydantic generators use it. The Scala and TypeScript generators ignore it for now, so their types describe one object.
Language strings
Section titled “Language strings”LinkML-Scala uses a fork of the metamodel that adds support for language strings in LinkML schemas.
These metadata slots of schema elements can be a plain string or a mapping from language tag to text:
title, description, keywords, aliases, comments, notes, deprecated, alt_description_text, value_description and local_name_value.
For example:
classes: Book: title: en: Book pl: Książka de: Buch description: en: It's a book, obviously.
Person: title: Person description: You can also still use the plain string syntax!Generators that write documentation in one language, such as the JSON Schema, GraphQL, TypeScript, Pydantic, Scala, Ossie and Frictionless generators, take the language from their metadataLanguage option (en by default).
When a text has no entry for that language, they use the en entry, or if there is none, the entry of the alphabetically first language tag.
A plain string is used as is, whatever language is requested.
In multivalued slots, such as keywords, this is done for each value separately, so one list can mix languages.
With metadataLanguage set to de, the Book class above gets the title Buch and the description It's a book, obviously..
The RDFS and SHACL generators emit plain strings as xsd:string literals and language mappings as rdf:langString literals:
library:Book a rdfs:Class ; rdfs:label "Book"@en , "Książka"@pl , "Buch"@de ; rdfs:comment "It's a book, obviously."@en .
library:Person a rdfs:Class ; rdfs:label "Person" ; rdfs:comment "You can also still use the plain string syntax!" .Schemas can also use language strings in their own data using the langstring type imported from linkml:types.
The value is a plain string or a non-empty mapping from language tag to text.
Inline type semantics
Section titled “Inline type semantics”Different forms:
Plain- In JSON the inlined class is always present. In RDF there MUST always be exactly one property representing this slot.Optional- in JSON the inlined class may be present, may be omitted or may be null. In RDF there MUST be at most 1 property.List- In JSON this is an array, which may be an empty array, may be omitted or may be null. No assumed constraints in RDF.Dict(Form)- In JSON this is aform(SimpleDictorCompactDict) object, which may be omitted, may be an empty object or may be null. No assumed constraints in RDF.
A slot can explicitly inline (inlined: true) a class, or implicitly inline a class, if the class does not have an identifier.
LinkML-Scala uses the following logic when a slot s is inlining a class c:
- If
sis not multivalued:- If
sis required, then the inline type isPlain, - If
sis not required, then the inline type isOptional.
- If
- If
sis multivalued:- If
shasinlined_as_list, then the inline type isList, - Else if
cdoes not have anidentifierorkeyslot, then the inline type isList, - Else if
chas 2 slots in total or 2 required slots, then the inline type isSimpleDict, - Otherwise, the inline type is
CompactDict.
- If
Null semantics
Section titled “Null semantics”We’re working on strict null semantics for LinkML instances - formalizing the usages of null, compatible with the expected metamodel structure.
(Not yet fully implemented)
CompactDict / SimpleDict:
{ key: null }(canonical) ={ key: {} }- Means
Map("key" -> RangeClass("key"))
- Means
{ key: [] }not allowed<omitted>(canonical) ={}=null- Means empty collection
Map()
- Means empty collection
List:
[ {} ]- Means a single instance with all default values
Seq(RangeClass())
- Means a single instance with all default values
[ null ]not allowed{}not allowed<omitted>(canonical) =null=[]- Means empty collection
Optional:
{}- Means slot is defined, with all default values
Some(RangeClass())
- Means slot is defined, with all default values
<omitted>(canonical) =null- Means
None
- Means
[]not allowed