Skip to content

Domain schemas

This page is for a bundle author whose domain needs types LOKF does not define. It shows how to write a LOKF domain schema, and how to make one from a published LinkML vocabulary.

Following OKF, a type that LOKF does not define is tolerated: the concept reads as lokf:Concept and the bundle stays valid. The type carries no meaning until a schema declares it.

A LOKF domain schema declares it. It is a LinkML schema that imports lokf.yaml and whose classes descend from Concept. Pass it as --schema to validate, convert, query and serve. The rules are in specification §6.2.

You have Do this
Types of your own Write a schema
A published LinkML vocabulary Adapt it
A vocabulary, and types or keys of your own Adapt it, then extend the copy
analytics.yaml
id: https://acme.example/schema/analytics
name: analytics
imports:
- linkml:types
- lokf # lokf.yaml, beside this file
default_prefix: analytics
prefixes:
analytics: https://acme.example/schema/analytics/
linkml: https://w3id.org/linkml/
classes:
Dashboard: # a concept LOKF has no class for
is_a: Concept
slots: [shows, refresh_interval]
slots:
shows: # a typed relation of the domain's own
range: Concept
multivalued: true
slot_uri: analytics:shows
refresh_interval:
range: string
  1. Put lokf.yaml beside the schema. The - lokf import reads it from there. Copy it from the installed package, so it matches the toolkit:

    Terminal window
    cp "$(python -c "from importlib.resources import files; print(files('lokf') / 'data' / 'lokf.yaml')")" .
  2. Put each class under Concept, directly or through a LOKF class such as Reference. The validator then accepts it as a type.

  3. Rename anything that shares a name with lokf.yaml, and keep its IRI. LinkML merges the two files into one namespace, so a Person or title of yours replaces LOKF’s. Rename yours and set its class_uri or slot_uri.

  4. Subclass a LOKF class to add a key to it. Classes are closed, so an undeclared key fails. Declare the key on the subclass and write the subclass’s name in type.

  5. Range each relation slot over Concept or a subclass of it. Its values then project as IRIs, and --check-refs resolves them. A slot ranged over string holds a literal.

  6. Pass the schema to each command:

    Terminal window
    lokf validate knowledge --schema analytics.yaml --check-refs
    lokf convert knowledge --schema analytics.yaml --format ttl
    lokf query knowledge --schema analytics.yaml "SELECT ?s WHERE { ?s a analytics:Dashboard }"
    lokf serve knowledge --schema analytics.yaml

With the schema, a Dashboard projects as a analytics:Dashboard and shows as analytics:shows. Without it, the same file reads as lokf:Concept with additionalType: Dashboard, and shows is a string. Both readings are conformant, and the bundle’s files are the same in each.

lokf adapt copies a published LinkML vocabulary into a LOKF domain schema. The copy imports lokf.yaml, renames each name the two share and keeps its IRI, and puts the vocabulary’s root class under Concept. Biolink-model is the worked case.

  1. Run lokf adapt on the vocabulary, with lokf.yaml beside it:

    Terminal window
    lokf adapt biolink_model.yaml
    biolink_model.yaml (Biolink-Model 4.4.4) -> biolink_model_lokf.yaml
    folded imports: attributes
    dropped for LOKF's: id, type
    detached from a dropped: category
    demoted designators: category
    renamed, IRIs kept: agent -> BiolinkAgent, dataset -> BiolinkDataset, unit -> biolink_unit, name -> biolink_name, ...
    folded into LOKF's: none
    re-rooted on Concept: Entity
    tree_root removed: BiolinkKnowledgeGraph, MappingCollection
    one canonical form: knowledge graph -> BiolinkKnowledgeGraph
    canonical names: 835
    verified beside lokf.yaml
    It is a LOKF domain schema: pass it as --schema, or import it from one of your own beside lokf.yaml:
    imports: [linkml:types, lokf, biolink_model_lokf]

    Each line is one of the moves. re-rooted on Concept is the one that makes biolink’s classes types: entity and the 264 classes below it can each be named in type. Biolink’s 51 mixins cannot; the classes that use them can.

    graph BT
        Gene -- is_a --> BiologicalEntity -- is_a --> NamedThing -- is_a --> Entity
        Entity -- "is_a (added)" --> Concept["lokf:Concept"]
  2. Write concepts with the vocabulary’s classes as type:

    knowledge/genes/brca1.md
    ---
    type: Gene
    category: [biolink:Gene]
    title: BRCA1
    biolink_name: BRCA1
    symbol: BRCA1
    xref: [HGNC:1100]
    in_taxon: [NCBITaxon:9606]
    status: stable
    ---
    # BRCA1

    type is LOKF’s designator, and validation matches the class by it. category is biolink’s own required list, so a biolink consumer reads the file too. biolink_name is biolink’s name, renamed in the copy; it still projects as rdfs:label.

  3. Pass the copy as --schema:

    Terminal window
    lokf validate knowledge --schema biolink_model_lokf.yaml --check-refs
    lokf convert knowledge --schema biolink_model_lokf.yaml --format ttl
    <https://acme.example/genomics/genes/brca1> a biolink:Gene ;
    rdfs:label "BRCA1" ;
    schema:name "BRCA1" ;
    schema:creativeWorkStatus "stable" ;
    biolink:category "biolink:Gene"^^xsd:anyURI ;
    biolink:in_taxon <http://purl.obolibrary.org/obo/NCBITaxon_9606> ;
    biolink:symbol "BRCA1" ;
    biolink:xref "HGNC:1100"^^xsd:anyURI .

    A slot ranged over a class (in_taxon) projects as an IRI. A slot ranged over uriorcurie (xref) projects as a literal. LOKF’s fields and relations work on the vocabulary’s classes: a LOKF Dataset can be about the gene, and --check-refs resolves it.

Name the root when the vocabulary has none

Section titled “Name the root when the vocabulary has none”

lokf adapt finds the root class by its identifier slot. A vocabulary whose classes carry no identifier has no root to find. Name its top classes with --root, one flag for each:

Terminal window
lokf adapt gist_core.yaml --prefix gist --root Aspect --root Category --root Component ...

gist-linkml, a LinkML rendering of Semantic Arts’ gist, is the tested case, because it fits this pattern. A concept then writes type: Requirement, which projects as a gist:Requirement.

lokf adapt folds a vocabulary slot into a slot on Concept when the slot’s slot_uri, or one of its exact_mappings, is that slot’s IRI. The copy drops the vocabulary’s slot, and the classes that used it use LOKF’s instead. A slot mapped to schema:name becomes title, whether the vocabulary spells schema.org with http or https. A class attribute folds the same way.

The fold copies the slot’s constraints into a slot_usage on every class that used the slot, so those classes still enforce what the vocabulary did: including inherited restrictions, class refinements, fixed values, cardinalities, patterns, and a narrower range such as uriorcurie. When two fields fold into one, both restrictions are retained using all_of where necessary. Retained child slots keep the restrictions of a removed parent. The fold is refused, and the report says why, when the slot’s shape differs from LOKF’s:

  • it is multivalued where LOKF’s slot is not, or the reverse;
  • its range is a class, an enum or a number where LOKF’s takes a string;
  • it represents a reference where LOKF expects an inlined object, or the reverse.

Use --fold for a slot, or a Class.attr attribute, that means the same as LOKF’s slot but declares no mapping. Use --keep for one whose mapping matches but which you want left as it is:

Terminal window
lokf adapt slsa.yaml --root Package --root SourceRepository --fold name=title --fold description=description

A concept then writes its display name once, as title, which projects as schema:name. Without the fold, the copy would carry slsa_name beside it.

lokf adapt warns about an attribute on a re-rooted class that has a Concept slot’s name but not its IRI. The attribute shadows LOKF’s slot on that class, and a record of the class writes the attribute. Fold it, or rename it in the vocabulary.

The copy is generated, so additions go in a schema of your own. That schema imports lokf.yaml and the copy, and --schema names it in place of the copy. The steps in Write a schema apply to what it adds.

gist attaches few slots to its classes, and classes are closed. A concept that needs a gist slot therefore takes a class that lists it:

enterprise.yaml
id: https://acme.example/schema/enterprise
name: enterprise
imports: [linkml:types, lokf, gist_core_lokf]
default_prefix: enterprise
prefixes:
enterprise: https://acme.example/schema/enterprise/
gist: https://w3id.org/semanticarts/ns/ontology/gist/
linkml: https://w3id.org/linkml/
classes:
GovernedRequirement: # gist's Requirement, with one gist slot listed
is_a: Requirement
class_uri: gist:Requirement
slots: [is_governed_by]
knowledge/requirements/rotate-keys.md
---
type: GovernedRequirement
title: Rotate keys
is_governed_by: [policies/security]
---
# Rotate keys
<https://acme.example/kb/requirements/rotate-keys> a gist:Requirement ;
schema:name "Rotate keys" ;
gist:isGovernedBy <https://acme.example/kb/policies/security> .

The copy has two inputs: the vocabulary’s release and lokf.yaml. Re-run lokf adapt when either changes, and commit the result. In CI, --check exits 1 when the copy is stale or no longer verifies:

Terminal window
lokf adapt biolink_model.yaml -o biolink_model_lokf.yaml --check