Blog

Proposal for a generic extensibility scheme for JSON data

Preliminary notes

Genericity

As its title implies, this document describes an extensibility scheme that is intended to be “generic”, meaning that it is not tied to any particular data model or application.

The scheme was designed with two specific applications in mind: the OME-Zarr imaging file format and an upcoming imaging metadata specification to be developed as part of the Imaging-PHD project.

However, the scheme should be applicable to any other data model that one wishes to make extensible with the same aims as those stated below.

Objects, models, schemas

This document will talk a lot about “objects”. In this context, an object is basically a JSON dictionary containing pre-determined keys.

The keys that an object can contain are defined by the object’s “model”, also referred to as the class of the object. Those keys are indistinctly called the fields, the attributes, or the slots of the class.

The use of the word “class” does not imply that this scheme is only intended to be used when working in a programming language that supports object-oriented programming. In this document, a “class” is merely an agreement that a given JSON dictionary is expected to contain a pre-defined set of keys, which are themselves expected to contain some pre-defined types of values. That agreement has no bearing on how the dictionary should be represented in a computer program; it can be represented using an actual class in a programming language that has such a concept (e.g. a Python class or a Java class), but it does not have to.

Likewise, when this document says that “class C inherits from class P” (or that it derives from P, or that it is a subclass of P), it merely means that C describes a dictionary that is expected to contain the same of pre-defined keys as P, plus any additional keys that are specific to C.

The world “model”, used alone, can also refer to the entire set of classes that describes all the objects that can be found in a given JSON document.

In the context of this document, a “schema” is a formal representation, using some specialised schema definition language (SDL), of a model or of a fragment of a model.

LinkML

LinkML is, among other things, a schema definition language.

The extensibility scheme described in this document has been designed, among other criteria, to be compatible with the use of LinkML as the schema definition language to represent the model that one wants to be extensible. Furthermore, some aspects of the scheme have been designed to explicitly benefit from the use of LinkML, notably in that those aspects would be easy to implement using LinkML – provided that your programming language of choice has decent support for LinkML.

That being said, the extensibility scheme is not dependent on LinkML. Its principles stand on their own, and it should be possible to implement it without ever using LinkML.

Full example

The following example will be used throughout this document to illustrate how the proposed extensibility scheme works. It is loosely based on the OME model’s “Instrument” branch, that describes a microscope.

{
    "manufacturer:": "Zeiss",
    "model": "LSM-880",
    "light_source":
    {
        "object_type": "https://example.org/ome/ArcLightSource",
        "power": 45,
        "lamp_type": "Hg"
    }
}

Here is what that example could look like, with an extension aiming to describe the kind of microscopes that one could find a long time ago, in a galaxy far, far away:

{
    "manufacturer": "Imperial Microscope Builders",
    "model": "K2SO",
    "light_source":
    {
        "object_type": "https://example.com/swm/KyberLightSource",
        "power": 900,
        "crystal_color": "red"
    },
    "extensions":
    {
        "https://example.com/swm/MicroscopeExtension":
        {
            "planet": "Coruscant"
        }
    },
    "extension_manifest":
    [
        {
            "id": "https://example.com/swm/",
            "version": "1.0",
            "schemas":
            [
                {
                    "type": "LinkML",
                    "url": "https://schemas.example.com/swm/swm.yaml"
                },
                {
                    "type": "JSONSchema",
                    "url": "https://schemas.example.com/swm/swm.json"
                }
            ],
            "homepage": "https://example.com/swm/"
        }
    ]

This example shows a single extension (https://example.com/swm/, also referred to in the rest of this document as “the SWM extension”) making use of the two types of extension mechanisms supported by this scheme: a natural extension class (https://example.com/swm/KyberLightSource) and a class extension (https://example.com/swm/MicroscopeExtension). Read on for details.

Goals

This extensibility scheme aims to achieve the following three goals:

Runtime data interoperability

The scheme is designed to allow for runtime data interoperability across implementations that support a different set of extensions (including the empty set, i.e. an application that supports no extensions) of a same “base” model.

Given a base model M and two extensions E1 and E2 (which would ideally all be expected to be represented as schemas in an appropriate schema definition language, though strictly speaking this is not necessary), we can define the following derived models:

The basic goal of “runtime data interoperability” is that an application that is only aware of the base model M can still read and manipulate data conformant to either of the three derived models, while (1) preserving any extended data it does not understand (the part of the data that is conformant with one of the extensions), (2) recognising the presence of extended data, and (3) offering the users some ways of accessing (and possibly manipulating) the extended data.

Likewise, an application that is only aware of the M(E1) model (base model + extension E1) should still be able to read and manipulate data that is conformant with all other models, under the same conditions.

This is of course not limited to only two extensions: the principle is generalizable to an arbitrary number of extensions.

Awareness of an extension

To fully understand the goal of runtime data interoperability, it is necessary to explain what is exactly meant by “an application aware of an extension” or “aware of an extended model”.

In this scheme, we consider three “levels” of awareness:

Compile-time awareness1 of an extension (or of an extended model) is when the developers of the application know about the extension during the development of the application, and write code to explicitly deal with data conformant with the extended model. Typically, this level of awareness will not only allow to read and manipulate the extended data, but also to offer specific features that explicitly make use of said extended data.

When schemas (such as LinkML) are used to formally describe the base model and its extension(s), compile-time awareness notably means that developers can rely on any code generation feature enabled by the type of schema used to automatically generate at least part of the code needed to read and manipulate the (extended) data.

Runtime awareness of an extension is when an application has been developed without explicit support for the extension (either because the developers did not know about that extension, or because they made a conscious decision not to implement it for whatever reason), but is able to dynamically learn about the extension at runtime. This requires that (1) a formal schema describing the extension can be obtained somehow (this should be the role of the “extension manageability” layer envisioned further below and (2) that the application is able to exploit said schema.

This level of awareness should allow an application to at least validate the extended data, even if it cannot make explicit use of it. Depending on the “richness” of the provided schema, it could also allow the application to display the extended data to the user in a more meaningful way.

When schemas are used, this level of awareness does not allow to use schema-powered code generation! By definition, the application is already running at that point, so code generation is no longer an option.2

Unawareness of an extension is when an application has been developed without explicit support for the extension and is unable to dynamically learn about the extension at runtime – either because a schema describing the extension cannot be obtained, or because the application is not equipped to exploit the schema (for example, if the extension comes with a LinkML schema, the application must have developed so that it can understand and exploit LinkML schemas).

This level of “awareness” (or rather this absence of awareness) obviously drastically limits what an application can do with the extended data it may find. However, an application that is compatible with this extensibility scheme, even if it is completely unaware of a particular extension, should still minimally be able to recognise the presence of extended data and ensure that it is fully preserved.

Guaranteeing independently developed extensions cannot clash

This extensibility scheme is expressly designed to ensure that, even if two extensions, which are being developed separately without any kind of coordination between their developers, extend the base model in an identical fashion (for example by adding a field with the same name, but possibly with a different meaning, to the same class), it is still possible to use both extensions at the same time in the same data, without any possible confusion or, worse, data loss.

Making extensions manageable

This extensibility scheme makes extensions manageable as first-class entities.

This notably means that, when a data file is making use of one or more extensions to the base model, an application (even an application that is not compile-time aware of any of the extensions being used) can know

Extensions as first-class entities are also intended to provide applications (and users) with additional information about the extensions, such as who is responsible for a given extension or where more information can be found about the extension.

Notably, “more informations about the extension” can include schemas that formally describe the extension, allowing an implementation that does not explicitly support the extension at compile-time to learn about the extension at runtime and to at least validate the extended data against a reference schema.

Non-goals

Controlling who can extend the base model

This extensibility scheme is not concerned about who is allowed to create an extension E? to a base model M. As far as this scheme is concerned, anyone can create an extension (and the scheme is explicitly designed with such a scenario in mind). Adding (and enforcing) restrictions about the source of an extension (for example, “only the authors of the base model can create an extension”, or “an extension can only be used after it has been formally approved by the International Steering Committee for the Base Model in one of its annual plenary session”) can easily be done if needed by an additional layer on top of this extensibility scheme.

Constraints on extensions

The requirement for data runtime interoperability across implementations with varying extension support creates some fundamental constraints on what an extension can do – about how the base model can be extended.

Simply put, an extension cannot change the base model in a way that would make data conformant to the extended model not conformant with the base model. So, an extension is really about literally extending the base model – the extended model should be a strict superset of the base model.

Therefore, an extension can:

An extension cannot:

Basic principles

Mechanisms for extending a model

This scheme distinguishes two mechanisms by which a base model can be extended:

They are not mutually exclusive: the same extension can extend the same model at different places using either of the two mechanisms at each place where it needs to extend the model.

Natural extension classes

A natural extension class exploits what this scheme calls a “natural extension point” in the base model. Basically, a natural extension point is a place in the base model where the model is already set up to allow for some variability.

To illustrate, let us consider again our base microscope example:

{
    "manufacturer:": "Zeiss",
    "model": "LSM-880",
    "light_source":
    {
        "object_type": "https://example.org/ome/ArcLightSource",
        "power": 45,
        "lamp_type": "Hg"
    }
}

We are interested in the light_source field, which expects an object that can be an instance of slightly different classes, each class representing a different type of “light source”. In the example above, the object is an instance of the https://example.org/ome/ArcLightSource class, representing a light source that is some kind of arc lamp.

Here is another example with a different light source object:

{
    "manufacturer:": "Zeiss",
    "model": "LSM-880",
    "light_source":
    {
        "object_type": "https://example.org/ome/LaserLightSource",
        "power": 45,
        "wavelength": 488,
        "laser_type": "HeNe"
    }
}

The light source object in this case is an instance of the https://example.org/ome/LaserLightSource class, which has the same power field than the https://example.org/ome/ArcLightSource class but has also some specific fields of its own (wavelength and laser_type).

The object_type field, which is also common to both classes, is what allows an application to recognise which type of light source it is dealing with, and therefore which fields to expect in the rest of the object.

In LinkML, the object_type field would be known as the type designator slot for all the classes representing light sources.

In the Rust language with the Serde library, the light_source field would be said to accept an internally tagged enum, with the object_type being the “tag”.

Other (de)serialisation libraries might refer to the same concept under other names. In this document, we will be using the LinkML terminology.

The important point here is that, even when just using the base model (before adding any extension to the mix), an application using the base model must already be prepared to deal with the facts that (i) there are several types (“classes”) of light source objects, and (ii) the exact type of light source used in a given Microscope object is not known in advance, but has to be discovered at runtime (by looking up the value of the object_type field). Therefore, it would be reasonably trivial to add a new type of light source to the pre-existing list of types. This is why we say that the model is naturally extensible at the light_source point (or that light_source is a “natural extension point”).

This scheme strongly recommends that model designers create natural extension points in their models whenever possible, as it is the simplest way to extend a model. All that is required to create a natural extension point is a base class (in the example above, the base class does not explicitly appear, but that is the class from which both the https://example.org/ome/ArcLightSource and https://example.org/ome/LaserLightSource are derived) with a type designator field.

When a natural extension point is available, a third-party can then extend the model simply by deriving new classes from the base class.

This is what our example “SWM extension” extension shown earlier in this document is doing. It creates a new light source class (which is a “natural extension class”) called https://example.com/swm/KyberLightSource, which can be used wherever a light source is expected:

{
    "light_source":
    {
        "object_type": "https://example.com/swm/KyberLightSource",
        "power": 900,
        "crystal_color": "red"
    }
}

Upon encountering a Microscope object containing such a light source:

Class extensions

The second extensibility mechanism allowed by this scheme is intended to allow adding new fields to a pre-existing class.3

In our running example, the SWM extension needs to add a planet field to the Microscope object (presumably representing the planet where the microscope has been manufactured). This is done as follows:

{
    "manufacturer": "Imperial Microscope Builders",
    "model": "K2SO",
    "extensions":
    {
        "https://example.com/swm/MicroscopeExtension":
        {
            "planet": "Coruscant"
        }
    }
}

The principle is that any class intended to be extensible has an implicit field named extensions specifically intended to store all the fields added by extensions.

That field is itself a dictionary where each key corresponds to a particular extension, and the value is an object fragment containing the fields added to the class by the extension (in this example, the planet field).

This principle naturally ensures that independently developed extensions cannot interfere with each other, even when used within the same instance data. If another extension also wanted to add its own planet field, along with maybe other fields of its own, this would look like this:

{
    "manufacturer": "Imperial Microscope Builders",
    "model": "K2SO",
    "extensions":
    {
        "https://example.com/swm/MicroscopeExtension":
        {
            "planet": "Coruscant"
        },
        "https://example.net/stm/MicroscopeExtension":
        {
            "planet": "Vulcan",
            "user": "Spock"
        }
    }
}

Of course, we would likely want to avoid having different extensions to represent the same thing; but preventing this situation from happening is out of reach of any extensibility scheme, if only because it is mostly a social problem – basically getting different people to agree on working together – rather than purely a technical one. The role of the extensibility scheme in that regard is merely to minimize the impact of such a situation, not to avoid it.

Upon encountering data where a Microscope instance contains a https://example.com/swm/MicroscopeExtension object fragment:

Extension management layer

The “extension management” part of this proposed scheme is intended, as its name implies, to fulfil the goal of making extensions manageable as first-class entities.

It relies on a simple data structure (hereafter called the extension definition structure) that can be used to describe an extension. Such a structure can then be used in any context where extensions need to be managed. Notably, this scheme proposes that the top-level object of a data file should include a extension_manifest key containing a list of extension definitions for every extension effectively used anywhere else in the data file (thereby providing implementations with a single place to look at in order to determine which extensions are needed to fully understand the entire file).

The same extension definition structure could also serve as the basis for a hypothetical, centrally managed “extension registry”, should such a registry be needed.

As currently envisioned, the extension definition structure would contain the following fields:

Each item in the schemas list is a simple, two-fields structure like this:

{
    "type": "LinkML",
    "url": "https://schemas.example.com/swm/swm.yaml"
}

where the type field identifies the type of the schema, and url is a link to the schema itself.

The scheme allows specifying schemas of different types (e.g. LinkML, JSONSchema, etc.) to avoid binding itself to a particular schema definition language. This does not mean that (i) all extensions should always provide a schema for each of the available schema definition languages (though providing more than one type of schema would increase the likelihood that an application can understand at least one of the schemas), and that (ii) applications should support all possible types of schemas (though supporting more than one type would again increase the likelihood that the application can understand at least one of the schemas provided by an extension).

Discussions

Customizations

As stated in a preliminary note, the scheme discussed here is intended to be generic. Among other things, this means that some aspects of it might be “customized” when the scheme is actually applied to a data model.

In particular, the scheme involves several special attributes that must be added to the data model that one wishes to make extensible, namely:

In this document, those attributes are named object_type, extensions, and extension_manifest, respectively.

Those names may or may not be sensible when the scheme is applied to a particular data model. If they are deemed not to be sensible, it is up to the model designers to pick alternative names as needed.

One may also consider prefixing all those names with some special character (e.g. ~object_type or %extensions) to highlight the special roles of those attributes and help distinguish them from all the other, non-special attributes present in the data model.

The name of the type designator attribute and the name of the attribute holding the extension object fragments may vary throughout the same model. For example, the designator attribute could be named object_type for one natural extension point and simply type in another natural extension point. We recommend always using the same name, but that may not always be practical especially when this scheme is applied to a pre-existing model (rather than a new model designed from scratch). When the name of the type designator attribute may vary from one extension point to another, it is up to the model designers to ensure that they always clearly specify which attribute is the type designator attribute for one particular extension point. Likewise for the attribute holding the extension object fragments.

On the use of URIs as identifiers

This scheme requires three different types of identifiers:

As shown in the running example used in this document, this scheme strongly recommends:

  1. that all identifiers follow a URI-based scheme, and
  2. that the identifiers for natural extension classes and for class extensions are built on top of the identifier of the extension they belong to.

For example, in the case of the SWM extension:

The first recommendation (using URI as identifiers) is intended to automatically ensure uniqueness of identifiers across all extensions, which is critical to ensure that independently developed extensions cannot interfere with each other.4

It also offers a straightforward way to distinguish between “official” extensions and “third-party” extensions (should such a distinction be desired – this scheme is neutral on that aspect), simply by declaring that all extensions whose base URI is in a given domain are “official”, and all extensions whose base URI is outside of that domain are third-party extensions. The people responsible for managing the data model can then control who should be allowed to create official extensions (again, should such a control be desired) by controlling who can allocate URIs in the “official domain”.

The second recommendation (building all the identifiers for a same extension out of the same base URI) offers a straightforward way to identify the extension a given natural extension class or class extension belongs to, without even having to look at the formal schemas that describe the extension (assuming the extension provides at least one such schema, and the application can understand it).

Both of those recommendations may be relaxed in a particular application of this scheme, but model designers must carefully weigh the consequences. In particular, allowing short, non-URI-based identifiers may produce slightly more “human-friendly” JSON (for as much as JSON can be human-friendly…) but removes all guarantees that independently developed extensions cannot interfere with each other – putting the onus on the people responsible for the model and on extension developers to ensure that it will not happen.

Versioning of extensions

The extensibility scheme currently does not strictly mandate how extensions should be versioned; it simply provides a way for extensions to self-describe their own version in the version attribute of their extension definition.

However, we strongly recommend that all changes to an extension should be constrained in such a way that data that is conformant to version X of an extension remains conformant with X+n.

Should an extension developer wish to create breaking changes in its own extension, the recommended way to do that is to effectively create a distinct extension, with its own base URI that must be different from the base URI of the original extension.

For example, should the SWM extension be updated so that the planet field added to the Microscope class is entirely removed and replaced by a new planets field accepting a list of values rather than a single value (maybe reflecting the facts that a single microscope may be made of parts manufactured on different planets), this should be considered a new extension. This could look like this:

{
    "manufacturer": "Imperial Microscope Builders",
    "model": "K2SO",
    "extensions":
    {
        "https://example.com/swm/v2/MicroscopeExtension":
        {
            "planets":
            [
                "Coruscant",
                "Alderaan",
                "Chandrila"
            ]
        }
    }
    "extension_manifest":
    [
        {
            "id": "https://example.com/swm/v2/",
            "version": "1.0"
        }
    ]
}

Why not use prefixed names for “class extensions”?

That is, why not doing something like this:

{
    "manufacturer": "Imperial Microscope Builders",
    "model": "K2SO",
    "swm:planet": "Coruscant",
    "stm:planet": "Vulcan",
    "stm:user": "Spock"
}

The author of this document strongly believes that such attempts to mimic XML-style qualified names in JSON or YAML are ill-inspired.

Qualified names work in XML because XML has built-in support for them! The concepts of qualified names and of namespaces are a core part of the XML specification, and any XML-compliant library must support them. JSON does not have any such concept (and neither does YAML), which means that applications would either need to hack their own namespace management feature, or accept that such “qualified names” do not in fact constitute a proper scoping mechanism.

“Hacking a namespace management feature” could look like this:
{
    "namespaces":
    {
        "swm": "https://example.com/swm/",
        "stm": "https://example.net/stm/"
    },
    "manufacturer": "Imperial Microscope Builders",
    "model": "K2SO",
    "swm:planet": "Coruscant",
    "stm:planet": "Vulcan",
    "stm:user": "Spock"
}

but this will never be directly supported by a JSON or YAML parsing library, meaning that applications will need to implement all the namespace management on their own.

Without a custom namespace management system, then “qualified names” are in fact just slightly longer names in a single, global, flat namespace. The presence of a colon does not automagically transform the global namespace into a tree of scoped namespaces. This is in fact no different than this:

{
    "manufacturer": "Imperial Microscope Builders",
    "model": "K2SO",
    "swm_planet": "Coruscant",
    "stm_planet": "Vulcan",
    "stm_user": "Spock"
}

which does not enclose the extension fields in their own namespace, it merely gives them longer names – with no intrinsic guarantee that no other extension developer will ever want to use the same “prefixes” swm_ or stm_.

Implementation in LinkML

This section illustrates how the scheme can be implemented in a data model that is formally defined using LinkML.

This is informative only. The scheme does not _need_ LinkML to be implemented, and even when LinkML is used it could possibly be done differently than what is proposed here.

Base model

Creating natural extension points

To create a “natural extension point” in a LinkML schema:

  1. Give a class a “type designator” slot;
  2. Explicitly configure the class to “allow extra slots”, and make sure it is not abstract.
  3. Any slot/attribute, anywhere in the schema, whose range is set to that class becomes de facto a natural extension point.

The type designator SHOULD be URI- or CURIE-typed. This scheme currently favours URI-typed designators, since they dispense from having to manage CURIE prefixes.

Some limitations currently exist in LinkML-Py that implementers should be aware of.

The most important is that, for now, instance data containing a type designator value that does not correspond to a known subclass of the class carrying the type designator will be considered invalid. This means that an application that is only aware of the base schema will always reject data containing a “natural extension” – basically making the entire concept of “natural extension” void! Work is in progress with the LinkML community to fix that issue.

Another issue (which may in fact be fixed at the same time as the previous one) is that, when generating Python code, the generated code will not include what is needed to properly recognise type designators when the schema does not contain at least one subclass of the class carrying the type designator (basically, the code generator assumes that, since there are no subclasses, there is no need for a way to designate the effective type at runtime).

Lastly (but much less importantly – it’s more an annoyance than anything else), because of another bug in some components of LinkML-Py, type designators for now should always be defined as global, schema-wide slots, rather than as class-specific attributes, even if the slot is only ever used in one class.

Example

The “light source” extension point in the running example of this document can be implemented in LinkML as follows:

slots:

  object_type:
    description: The type designator for natural extension points.
    range: uri
    designates_type: uri

classes:

  LightSource:
    description: The base class for all light source objects.
    slots:
      - object_type
    attributes:
      power:
        range: integer
      # All other attributes common to all light source types
      # ...
    extra_slots:
      allowed: true

  ArcLightSource:
    description: Represents a light source that is specifically an arc lamp.
    is_a: LightSource
    attributes:
      # Attributes specific to this type of light source
      # ...

  LaserLightSource:
    description: Represents a light source that is specifically a laser.
    is_a: LightSource
    attributes:
      # Attributes specific to a laser-based light source
      # ...

  Microscope:
    attributes:
      light_source:
        description:
          The light source available on this microscope.

          This is the actual extension point.
        range: LightSource
      # Other attributes for the Microscope class
      # ...

Allowing class extensions

To allow classes from the base schema to be extended according to the mechanism described in this scheme, the base schema must:

  1. define a base class (hereafter called ExtensionNode, but the name does not really matter) that all extensions shall derive from (that class must have a type designator);
  2. ensure that any class that is intended to be extensible has a dedicated extensions field to store the extension object fragments.

Here is a possible implementation:

slots:

  extension_type:
    description: The type designator for all extension nodes.
    range: uri
    designates_type: true
    key: true
    alias: type

classes:

  IsExtensibleMixin:
    description:
      An object that can carry arbitrary extension object fragments.

      Reuse this mixin in any class to allow the class to be
      extended.
    mixin: true
    attributes:
      extensions:
        description: The collection of extension object fragments.
        range: ExtensionNode
        multivalued: true
        inlined: true
        inlined_as_list: false

  ExtensionNode:
    description:
      Represents a fragment of an object that is defined by an extension
      rather than by the base schema.
    slots:
      - extension_type
    extra_slots:
      allowed: true

You may notice at this point that the extensions attribute looks similar to a natural extension point, since its range is set to a class that has a type designator. This is because it is indeed a natural extension point! Conceptually, the way this extensibility scheme allows extending classes is by giving each class (or at least, each class reusing the IsExtensibleMixin mixin) a “generic” natural extension point.

Applying this to our running example, when we define the Microscope class we only need to make it inherit from the IsExtensibleMixin, which will give it the extensions field intended to store the extension object fragments:

classes:

  Microscope:
    mixins:
      - IsExtensibleMixin
    attributes:
      # all the attributes for the Microscope class defined in the
      # base model...

Extended model

Creating a natural extension

If the base model has a natural extension point (such as the light_source field in the Microscope class we have seen above), then creating a natural extension intended to be used at that point is simply a matter of:

  1. importing the base schema;
  2. creating a new subclass of the appropriate base class.

For example, to create the KyberLightSource extension:

classes:

  KyberLightSource:
    description:
      A light source that uses a Kyber crystal; allows to see things
      through the Force, but can only be used by Jedi microscopists.
    is_a: LightSource
    attributes:
      # Attributes that specifically describes this type of light source
      # ...

Of note, the class does not need to derive directly from the base, it can derive from another subclass if it happens to be more appropriate.

Creating a class extension

This is how the https://example.com/swm/MicroscopeExtension extension object fragment (which aims to add a planet field to the base Microscope class) could be implemented:

classes:

  MicroscopeExtensionMixin:
    description:
      A mixin that contains the attributes we want to extend the
      Microscope class with.

      This is a mixin in order to facilitate extension composability.
    mixin: true
    attributes:
      planet:
        description: The planet where the microscope was manufactured.

  MicroscopeExtension:
    description:
      The actual extension object. This (i) inherits from ExtensionNode
      (so that it is recognized as an extension object fragment) and
      (ii) reuses the MicroscopeExtensionMixin above.
    is_a: ExtensionNode
    mixins:
      - MicroscopeExtensionMixin

  ExtendedMicroscope:
    description:
      This is a subclass of the base Microscope class with the added
      MicroscopeExtensionMixin.

      This class is not, strictly speaking, needed for the extension
      scheme described in this document, but its presence will allow
      an application to support the MicroscopeExtension “natively”,
      by using this class whenever the use of the Microscope class is
      expected.
    is_a: Microscope
    mixins:
      - MicroscopeExtensionMixin
  1. Despite its name, the notion of “compile-time” is not reserved to applications written in compiled languages (e.g. C, Java, Rust…). It applies to interpreted languages (e.g. Python, Javascript…) as well, though for such languages it is, strictly speaking, improper (it would be more appropriate to talk about “development-time”; however, “compile-time” is the commonly accepted term).
  2. Some programming languages or frameworks may in fact allow the dynamic generation and loading of code at runtime (for example the System.Reflection.Emit system in the .NET framework). We do not consider such cases here.
  3. As noted in the section about constraints, an extension could also tighten constraints on existing fields of a class. However this kind of modification has no impact on how the data is serialised, so it does not require any particular attention here. Informing applications of the new, tightened constraints would be the role of the schema(s) formally describing the extension, to be provided by the extension management layer.
  4. The implicit assumption here is that extension developers will be well behaved and will not use URIs that they do not control.