Code generation for use with the LinkML-Java runtime

In order to be usable with the LinkML-Java runtime, the code that represents a LinkML model in Java must have been generated according to certain rules.

LinkML-to-Java translation rules

LinkML types

LinkML’s basic types are rendered in Java as follows:

LinkML type Java type Notes
string String
integer Integer (1)
float Float (1)
double Double (1)
boolean Boolean (1)
datetime java.time.ZonedDateTime
date java.time.LocalDate
time java.time.LocalTime
uri java.net.URI
uriorcurie String (2)

(1) Those types may also be represented by their primitive equivalent (int, float, double, and boolean) if and only if the corresponding slot is required. If the slot is not required, then the boxed types are used instead so that we can represent the fact that the slot may not have a value (by setting the field to null).

(2) The LinkML uriorcurie type is represented by a standard Java String object, rather than by a dedicated class, for convenience. To support automatic expansion/contraction of CURIEs, the String field representing a LinkML uriorcurie slot must be annotated with a @Converter(CurieConverter.class) annotation (more on that below).

LinkML classes

A LinkML class is represented by a Java class that must satisfy the following constraints:

  • The class must have a public no-argument constructor. It may have additional constructors as needed but the no-argument constructor must be present and publicly accessible.

  • Each slot is represented by a (typically private) field which must have public, predictably named read and write accessors.

The general naming rule for accessors is that a slot named foo must have a getFoo() read accessor and a setFoo() write accessor.

Two specific rules apply to required, boolean-typed slots:

  • the read accessor of a boolean-typed slot named foo must be isFoo(), rather than getFoo();

  • if the name of the slot already starts with is followed by an uppercase letter, that is prefix is removed before applying the previous rule (so for example, a slot named isFoo must have a read accessor that is also named isFoo() and a write accessor that is named setFoo, as if the slot had been named simply Foo).

Those rules were chosen because they correspond to the behaviour of the Lombok annotation processor, which can then be used to easily generate the accessors.

A multi-valued slot of type T is represented as a List<T> field. Importantly, this is always the case, even if the slot may be serialised as a dictionary. The fact that the slot may be serialised as a dictionary has no bearing on how it is represented in memory.

LinkML enumerations

“Simple” (non-dynamic) LinML enumerations are rendered in Java as standard Java enum objects.

The one constraint is that the Java enumeration must have a static fromString(String) method that can turn a String value into one of the possible enumerated value (or into null if the given String is not a permissible value).

Dynamic LinkML enumerations are currently completely unsupported, both on the side of the LinkML-Py code generator and on the side of the LinkML-Java runtime. How to represent such enumerations in Java and make them work as expected is still an open question, but one thing that is almost certain already is that they will not be represented as standard Java enum objects, because the values of a Java enum must necessarily be fully known at compile-time – this is by design incompatible with the very idea of a “dynamic enum”.

A possible workaround for dynamic enums for now is to use a tool such as vskit expand to generate the permissible values at compile-time, in effect turning a dynamic enum into a non-dynamic one – but of course by doing so you lose all the “dynamic” aspects.

Java annotations

One principle that underpins the LinkML-Java runtime is that it must not require runtime access to the LinkML schema – therefore, the runtime must have a way to access some of the informations from the schema, solely from the generated code.

This is done via LinkML-specific Java annotations that must be present in the generated code.

All annotations reside in the org.incenp.linkml.core.annotations Java namespace. They are:

Annotation Purpose
LinkURI Provide the URI associated to a class or a slot
Identifier Mark the identifier or key slot
TypeDesignator Mark the slot that designates the runtime type of an instance
Required Mark a slot as being required or recommended
Inlined Mark a slot as being inlined (as list or as dictionary)
SlotName Provide the original name of a slot
Converter Indicate that the class or slot requires a custom converter

Example

Here is an example of a class ready to be used with the runtime (import declarations omitted for brevity):

@LinkURI("https://example.org/myschema/classes/MyClass")
public class MyClass {

    // The unique identifier for objects of that class
    @Identifier
    @LinkURI("https://example.org/myschema/slots/id")
    private String id;
    
    // Contains the runtime type of an object (to distinguish between
    // the various subclasses of MyClass)
    @TypeDesignator
    @LinkURI("https://example.org/myschema/slots/type")
    private String type;
    
    // The values of this slot (instances of AnotherClass) are expected
    // to be inlined (rather than referenced) as a list (rather than as
    // dictionary)
    @Inlined(asList = true)
    @LinkURI("https://example.org/myschema/slots/foos")
    public List<AnotherClass> foos;
    
    // Accessors for all the slots
    public String getId() {
        return id;
    }
    
    public void setId(String id) {
        this.id = id;
    }
    
    public String getType() {
        return type;
    }
    
    public void setType(String type) {
        this.type = type;
    }
    
    public List<AnotherClass> getFoos() {
        return foos;
    }
    
    public void setFoos(List<AnotherClass> foos) {
        this.foos = foos;
    }
}

Generating the Java code

In the future, the LinkML-Java project will most likely provide its own code generator, that will automatically produce code ready to work with the runtime. But for the time being, we are dependent on the Java code generator provided by the LinkML project itself.

LinkML-Java developers are actively working with the upstream LinkML project to ensure that LinkML-Py’s code generator can produce code meeting all the requirements of the LinkML-Java runtime. This is done through a dedicated “template variant” called org.incenp.linkml, whose use can be requested on the command line as follows:

$ linkml generate java --template-variant org.incenp.linkml ...

Of note, currently this requires a development version of LinkML-Py, as the amendments to the Java code generator have not been part of a released LinkML-Py version yet.