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)
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
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.
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 Inherited boolean expressions for details and examples.
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 |
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).
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/
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.
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.
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.
The identifier slot for classes must have a scalar type range.
It is an error to have an enum or class identifier.
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.
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-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.
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
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