Object-Oriented Linked Data (OO-LD) connects the structural modelling of objects and subobjects with the modelling of their semantic relations, by combining two existing standards - [[JSONSCHEMA]] and a [[JSON-LD11]] context - in a single document. A single OO-LD document is at once a valid JSON Schema and a reference-able JSON-LD remote context, so that one source can drive validation, RDF generation, code generation, user interfaces and API definitions using the existing JSON Schema and JSON-LD tooling.

This is a community specification produced by the OO-LD project. It is a work in progress and may change at any time. It is not a W3C Standard nor is it on the W3C Standards Track. Feedback and issues are welcome in the issue tracker.

A companion, example-driven guide (primer) introduces the same concepts in a didactic, step-by-step form. This document is the normative specification.

Introduction

OO-LD Schema aims to connect the structural modelling of objects and subobjects with the modelling of the semantic relations without reinventing the wheel. It therefore combines existing standards, primarily JSON Schema and a JSON-LD context in the same document.

What distinguishes OO-LD from other approaches that unify structure and semantics is that the artefact is the source. A single OO-LD document is at the same time a valid JSON Schema and a referenceable JSON-LD remote context, so there is no separate modelling language to learn and no build step that generates - and then has to keep in sync - a separate schema and a separate context. The same property extends to the instance level: an OO-LD instance is a valid JSON-LD document, and OO-LD-aware tooling (for example the oold library) resolves the IRIs it references into linked objects, so a stored graph can be navigated as objects rather than as loose identifiers.

Design Goals and Rationale

Compatibility

An OO-LD document is always a valid JSON document. This ensures that all the standard JSON libraries work seamlessly with OO-LD documents.

An OO-LD instance document is always a valid JSON-LD document. This ensures that all the standard JSON-LD libraries work seamlessly with OO-LD instance documents.

An OO-LD schema document is always both a valid JSON Schema document and JSON-LD remote context. This ensures that all the standard JSON Schema and JSON-LD libraries work seamlessly with OO-LD schema documents.

Expressiveness

An OO-LD schema document allows the developer to express the syntax of a JSON instance document side by side with its semantics in a single source.

In addition, syntactical and semantic definitions can also be applied to external JSON instance documents that reference OO-LD schema documents.

This allows to specify well-defined patterns in a directed graph and enables tools relying on a hierarchical object structure to produce data for and consume data from such a graph.

Interoperability

OO-LD schema documents allow to specify all information that is needed to automatically transform data between semantically equivalent but syntactically different notations.

Positioning

Data modelling is commonly described in three layers: a conceptual layer (RDFS, OWL), a logical layer (SHACL, ShEx) and a physical layer (JSON Schema, XML Schema). Keeping these layers consistent when they are authored separately is a recurring problem. OO-LD is a physical-layer artefact that carries enough conceptual annotation - its JSON-LD @context together with keywords such as x-oold-range (see ) - to generate the logical layer (SHACL shapes) and the conceptual layer (OWL) from the same source, so the layers cannot drift apart.

OO-LD does not aim to replace established modelling frameworks. Frameworks such as LinkML, SPDX 3.0 and TreeLDR compile a bespoke source into JSON Schema and a separate JSON-LD context; OO-LD instead annotates JSON Schema in place and offers generated bridges to the forms other ecosystems expect. A consumer that cannot accept a top-level @context - OpenAPI 3.0, or a strict LLM structured-output subset - is served a generated form (x-jsonld-context / x-jsonld-type, or annotations folded into title / description; see ) rather than a hand-maintained parallel document, so the single OO-LD source stays authoritative. This positions OO-LD as an interlingua in the physical layer, not as another isolated syntax.

Structure first, semantics deferred

A JSON Schema keyword names a property with a local string; an RDF predicate is a global IRI. RDF-based schema languages such as SHACL therefore require every participating term to be a committed, globally named IRI before data can be expressed or validated as a graph: RDF has no notion of a field whose meaning is not yet decided. This is not a question of how many vocabularies are involved - SHACL validates across many vocabularies at once - but of when the commitment must be made. In domains where a shared vocabulary is missing, contested, or still emerging, that eager, per-term commitment is a barrier to starting.

OO-LD inverts the order. Because it is JSON Schema first, a term exists as a plain structural property with no semantics attached; an author MAY leave it unmapped, map it to a single IRI, or map it to several concurrent IRIs (see and ). An unmapped term produces no triples under JSON-LD expansion; to prevent such terms from being dropped silently, an author MAY declare @vocab in the @context, which maps every otherwise-unmapped term to a default (local) namespace so it still round-trips to RDF under a provisional IRI until a curated mapping is assigned. Any data can be serialized as JSON and therefore described by a schema, so the structural model - validation, forms, code generation, transport - is always available, and semantics can be added later, incrementally, one term at a time. The floor on this deferral is identity, not zero: graph-shaped data still needs identifiers for its links (carried by IRI-valued properties and ), but the vocabulary and meaning of each field can be deferred without blocking any of the structural uses. This is the boundary to RDF/SHACL: they cannot withhold semantic commitment, OO-LD can.

Conformance

As well as sections marked as non-normative, all authoring guidelines, diagrams, examples, and notes in this specification are non-normative. Everything else in this specification is normative.

The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in [[!RFC2119]].

Terminology

The following terms are used throughout this specification:

OO-LD schema
A document that is simultaneously a valid JSON Schema and a reference-able JSON-LD remote context.
OO-LD instance
A JSON document that conforms to an OO-LD schema and is itself a valid JSON-LD document.
remote context
A JSON-LD context referenced by URL, as defined in [[JSON-LD11]] §3.1.
resolved schema
A single, merged view of a composed schema (see ).
range
A type constraint on an IRI-valued property (see ).
reverse property
A property that is stored on and edited from both ends of a relation (see ).

Basic Concepts

The core idea is that an [=OO-LD schema=] is always both a valid JSON Schema and a reference-able JSON-LD [=remote context=] as defined in [[JSON-LD11]] §3.1 (not a JSON-LD document). In this way a complete OO-LD class / schema hierarchy is consume-able by JSON Schema-only and JSON-LD-only tools while OO-LD-aware tools can provide extended features on top (e.g. UI autocomplete dropdowns for string-IRI fields based on a SPARQL backend, or SHACL shape / JSON-LD frame generation).

There is an asymmetry between how schemas and instances are consumed:

This asymmetry is what lets a single document serve both as a JSON Schema $ref target and as a JSON-LD remote @context for the same resource. Concretely: an instance is processed directly as a JSON-LD document (e.g. jsonld.toRDF(instance)), which loads the schema as a remote context via the instance's @context; a schema is only ever referenced as that context and MUST NOT itself be expanded as a document.

A class B extends a class A by referencing it in both allOf (so JSON Schema validators apply A's rules when validating B instances) and @context (so JSON-LD processors resolve A's term mappings). B instances are therefore valid A instances and carry all of A's properties alongside B's own additions. Building types from multiple independent schemas is covered in .

Composition

Composition is how an [=OO-LD schema=] incorporates multiple independent schemas, each contributing its own properties and JSON-LD term mappings, without requiring a shared parent class. This lets you build complex types by assembling reusable building blocks - for example, attaching a geolocation schema and a contact schema to a single resource type. The rules below govern how the resulting @context is assembled automatically, so that no post-processing step is needed.

It MUST NOT be required to further process an OO-LD schema document in order to interpret it as a JSON-LD context. This implies that all occurrences of $ref in the schema are reflected in the JSON-LD context. $ref within properties of type: object MUST be listed as a scoped JSON-LD context. $ref within all other property types and at the root level of the OO-LD schema MUST be listed at the root level of the JSON-LD context. In case of multiple $ref within allOf the corresponding remote contexts are merged into an array-valued @context (see ). For oneOf / anyOf this requires care to avoid conflicts. At any time the importing OO-LD schema MAY define its own or override the imported JSON-LD context.

Merging remote contexts

Multiple $ref (e.g. in allOf) each correspond to a remote context. By the reflection rule above, the schema's own @context MUST list those remote contexts as an array, in the same order as the allOf members, so the schema stays usable as a context without further processing. A JSON-LD processor then resolves that array in order, later entries overriding earlier ones - duplicate context terms are overridden using a most-recently-defined-wins mechanism ([[JSON-LD11]] §4.1.5). The schema MAY append its own context object as the last array entry to override an inherited term. The single-context @import keyword is an alternative only when exactly one remote context is wrapped and locally modified (it cannot contain a nested @import), so the array form is used for the multi-$ref case.

oneOf / anyOf. The remote contexts of oneOf / anyOf branches MAY also be reflected into the @context, but they MUST NOT conflict at the root - they MUST NOT map the same keyword to different IRIs there. A JSON-LD processor merges all listed contexts (most-recently-wins) and has no notion of which branch a given instance matched, so a root-level conflict would be decided by context order rather than by the branch the data conforms to.

Where branches genuinely need different mappings for the same keyword, do not place them at the root; scope them so each mapping applies only where its branch applies, using JSON-LD scoped contexts:

Here { "@type": "Sensor", "reading": 21 } maps reading to ex:temperature, while { "@type": "Gauge", "reading": 3 } maps the same keyword to ex:pressure.

Propagation (@propagate). A $ref inside a type: object property is reflected as a property-scoped context, which by default propagates into the whole subtree rooted at that property ("By default ... contexts propagate across node objects, other than for type-scoped contexts, which default to false"). Where a referenced context should apply only to the immediate node, the schema MUST set "@propagate": false on that scoped context. Contexts combined in a single array MUST share the same @propagate value.

Protected terms (@protected). A schema MAY mark terms @protected to prevent later contexts from silently redefining them. When contexts are combined via allOf, redefining a protected term to a different IRI is an error unless the new definition is identical; property-scoped contexts are exempt and may override protected terms within their subtree. Relying on @protected therefore constrains which schemas a schema can be combined with.

Independent references and base URIs. A JSON Schema $ref and a JSON-LD @context entry are independent references: they MAY point to the same document (the typical OO-LD case, where one document is both a schema and a context) or to different documents - for example a plain JSON Schema referenced via $ref together with a separate remote @context that supplies the semantics. Relative references resolve against the schema's $id (the JSON Schema base URI) and, on the JSON-LD side, against @base / the retrieval URL; these base URIs SHOULD be aligned so a relative reference resolves to the same absolute URL under both. $id MUST NOT contain a non-empty fragment ([[JSONSCHEMA]] §8.2.1).

Merge and override model

JSON Schema's validation algebra is purely conjunctive: allOf requires an instance to satisfy every subschema, and the specification defines no merge of the subschemas' keyword values ([[JSONSCHEMA]] §10.2.1.1). Several consumers nonetheless require a single [=resolved schema=] view rather than a conjunction: a UI generator needs exactly one title per field, a code generator flattens an inheritance chain into one class, and an OO-LD preprocessor must produce one @context.

When such a merge is required, OO-LD resolves the allOf chain by applying JSON Merge Patch ([[RFC7396]]) semantics: keyed by object member, most-recently-defined (most-derived) wins, and a null value removes a key. For the @context this coincides with JSON-LD's own override rule. For assertion-bearing keywords the resolved view additionally honors narrow-only composition: a derived schema MAY restrict a constraint but MUST NOT relax it, matching how code generators let a subclass tighten - never loosen - a superclass property's validation.

This single model governs every place OO-LD collapses composition into a resolved value: single-valued annotations such as title and x-oold-multilang-title resolve most-derived-wins, constraints resolve narrow-only, and additive maps merge by key. A preprocessor applying it produces exactly the view that UI and code generators already compute, so no separate "resolved schema" format is needed.

Closing composed objects

Because allOf composition is conjunctive, additionalProperties: false on one subschema rejects the members that the other composed subschemas contribute, so it cannot close a composed object. To close such an object - rejecting members that no composed subschema declares - use unevaluatedProperties: false ([[JSONSCHEMA]] §11.3), which is evaluated after the allOf branches and therefore accounts for their properties. A schema closed this way should still allow the instance-level $schema and @context members (see ).

Schema Instances

An [=OO-LD instance=] is a JSON document that conforms to an [=OO-LD schema=]. It references that schema in two ways, both pointing at the same (preferably versioned) schema URL:

Instances SHOULD use a versioned schema URL so that it is unambiguous which schema version they conform to.

Referencing the schema with $schema

In standard JSON Schema, $schema identifies the dialect (meta-schema), not the schema an instance validates against ([[JSONSCHEMA]] §8.1.1); JSON Schema does not define an in-band way for an instance to point at its own schema. OO-LD therefore uses $schema on instances as a convention: standard JSON Schema validators treat it as ordinary data, while editors (VS Code, JetBrains, JSON Schema Store) and CI checks (e.g. check-json-schema-meta) honor it. Where the instance is served over HTTP, the standards-conformant alternative is the describedby link relation ([[JSONSCHEMA]] §9.5.1.1), optionally with the profile media-type parameter ([[RFC6906]]):

An OO-LD-aware tool determines an instance's schema in the following order:

  1. the $schema value, if present;
  2. otherwise, the URL given under @context, if the referenced document declares itself to be an OO-LD schema;
  3. otherwise, an inline @type (see below) - but only when at least one of the type IRIs resolves to an OO-LD schema.

An implementation MAY additionally maintain a registry mapping rdf:type IRIs to OO-LD schemas to resolve case 3, but such a registry MUST NOT be assumed to exist on the consuming side - so exports must be self-sufficient (see below).

Because an instance carries $schema and @context as ordinary members, an OO-LD schema that closes its objects with additionalProperties: false or unevaluatedProperties: false MUST permit these two members, or conforming instances would fail validation.

@context already provides a JSON-LD-native link to the schema (resolution case 2 above), so $schema is kept primarily for compatibility with the widespread editor and CI convention, not as a second authoritative mechanism. JSON Schema deliberately does not standardize $schema on instances, partly over a self-validation concern: a consumer SHOULD NOT blindly trust the schema an instance declares for itself (a crafted instance could point at a permissive schema) and remains responsible for validating against a schema it trusts.

Identity (@id)

An instance that represents an identifiable entity is identified by an @id - the IRI of that entity. This is the JSON-LD node identifier, distinct from $schema / @context (which identify the schema) and from the schema's own $id / x-oold-uuid. Without an @id the entity is an anonymous blank node and cannot be referenced (for example as the target of an x-oold-range or @reverse relation).

To keep instance keys variable-name-friendly, schemas SHOULD expose @id through an aliased id property (as with type -> @type):

An implementation MAY use a non-IRI identifier internally, but when it exports an identifiable entity (to JSON-LD / RDF) it MUST assign an @id (or the aliased id). The @id SHOULD be resolvable, and it is RECOMMENDED to mint it from an autogenerated UUID - mirroring the schema's x-oold-uuid - e.g. https://example.org/a1b2c3d4-1234-....

Embedded value objects that have no independent identity (for example an Address embedded in an Organization) MAY omit @id and remain blank nodes.

Carrying the semantic type

The type and semantics of an instance are owned by the OO-LD schema it references; an instance is therefore not required to carry an inline type, and a schema version may remap to different ontology terms without rewriting existing data.

A schema declares the rdf:type(s) of its instances with the x-oold-instance-rdf-type keyword (always a list of IRIs, e.g. ["schema:Person"]):

These types live in the schema, not in the instance data, so a JSON-LD-only processor - which sees only the instance and its @context - cannot derive them. Therefore, when OO-LD tooling exports an instance (to JSON-LD / RDF), it MUST materialize the declared rdf:type(s) as an @type on the instance, so that the type reaches RDF without access to the schema or to a type registry.

Alternatively, an instance MAY carry the type inline as a type property (self-describing data). The schema maps the type term to the JSON-LD keyword @type with a simple alias and gives it a default; the value MAY be a single IRI or a list of IRIs, and this property named type is distinct from the JSON Schema type keyword. No @type: @id coercion is needed because JSON-LD already interprets @type values as IRIs:

const would also fix the type, but it would prevent a subclass from overriding or extending it (for example a subclass adding schema:Researcher). With default a subclass can redefine the property.

If an inline type is present it MUST be consistent with the schema's x-oold-instance-rdf-type. Note that @type alone lets a consumer locate the schema (case 3 above) only when one of the type IRIs resolves to an OO-LD schema.

Under composition, x-oold-instance-rdf-type follows the same most-derived-wins rule as the rest of the schema (see ): the nearest declaration in the allOf chain is authoritative and REPLACES - it does not append to - a base class's value. Superclass types are recoverable by ontology inference (rdfs:subClassOf) and so need not be materialized; a schema that wants a supertype carried in the data lists it explicitly (e.g. ["schema:Researcher", "schema:Person"]).

Property value forms

A property whose value denotes another resource takes one of three forms in an instance, distinguished by the shape of the JSON value, and each projects to RDF differently:

A single @context term cannot interpret a bare string as both a literal and an IRI: @type: "@id" coerces every string value to an IRI (so free text becomes an - often invalid, then dropped - IRI), while a plain term keeps every string a literal. A property whose range is references only therefore uses @type: "@id" and MAY be written as a bare IRI string; a property whose range includes free text MUST NOT use @type: "@id".

For a property whose range mixes free text with references and/or embedded objects (for example Text | PostalAddress | Place), two patterns keep the instance round-trippable (see ); a model ecosystem SHOULD adopt one of them consistently:

  1. Value-form - a single plain term (no @type: "@id"); the value shape alone disambiguates: a bare scalar is a literal, { "id": ... } is a reference, a typed object is embedded. References are written as objects, and the term MUST NOT carry @type.
  2. Separate keys - a canonical term p with @type: "@id" (a bare IRI string reference, plus embedded objects via a scoped @context) and a companion p_text that is a plain term for the literal.

xsd:string is RDF's default datatype and is elided from plain string literals, so a term declaring @type: "xsd:string" is never selected when a string value is compacted back from RDF - the value would reappear under the full predicate IRI instead. A literal term MUST be left plain (no @type). Non-default datatypes (xsd:date, xsd:integer, xsd:float, ...) stay explicit on the literal and do round-trip, so datatype coercion for those is correct.

Whichever pattern is chosen, x-oold-range records the permitted target type(s) on the reference and embedded branches (see ).

A reference can carry sibling members beside @id (a cached label, coordinates, ...) for denormalization or offline display. In RDF these become ordinary assertions about the referenced IRI, indistinguishable from authoritative statements - their status as a cache is not recoverable from the data, and a generic consumer reads them as authoritative. Treating them instead as indicative, potentially outdated copies whose authoritative values live on the referenced target (so the target's own property wins on conflict) is a contract an implementation or ecosystem adopts, not something the specification can impose on every consumer. An ecosystem that stores such cached values should state the contract explicitly, and may mark cached members - for example with a dedicated annotation - so that consumers which do not share the contract can still tell them apart from authoritative data.

Projection to RDF and round-trip

An OO-LD instance projects to RDF through its @context and can be reconstructed from RDF. Because RDF is an unordered set of triples over a flat graph, faithful reconstruction follows a fixed contract:

A datatype-coerced value round-trips by RDF value, not byte-for-byte: e.g. the JSON number 40.75 under @type: "xsd:float" becomes the canonical lexical form "4.075E1" and returns as that string. Round-trip equality for typed literals is value equality after datatype canonicalization.

Identification and Versioning

Identification

OO-LD schemas MUST have a $id ([[JSONSCHEMA]] §8.2.1) which works as a global and unique identifier of the schema. The value of $id MAY be an absolute URI (details below). The schema SHOULD be resolvable via this URI. The schema SHOULD have an annotation x-oold-uuid with a UUID value.

Ontology class IRI (x-oold-iri)

x-oold-iri declares the IRI of the ontology class that this schema realizes - the RDF/OWL class from an external vocabulary that gives the schema its semantic grounding. It is distinct from two related IRIs:

In this example, the schema document is fetched from the $id URL, it realizes the ontology class schema:Person, and instances exported to RDF carry @type: schema:Person. The most common case is that x-oold-iri and the entries in x-oold-instance-rdf-type resolve to the same IRI, but they may differ - for example when a schema models a more specific subclass inline while still emitting a broader rdf:type on instances.

OO-LD-aware tooling uses x-oold-iri to anchor the schema in an ontology graph, independently of where the schema document is hosted - for example to resolve super-classes, look up ontology annotations, or generate SHACL shapes.

Versioning

The schema version SHOULD be indicated by x-oold-version; a prior version MAY be indicated with x-oold-prior-version:

The version SHOULD be part of the schema's location:

Since a package combines multiple schemas, the package version does in general not match the individual schema version.

Schemas MAY indicate explicit backward-compatibility with x-oold-backward-compatible-with and x-oold-incompatible-with:

Schemas within a package or package repository MAY use relative URIs ([[RFC3986]] §5.1). For example, A.schema.json referenced from https://raw.githubusercontent.com/MyOrg/my-package/refs/heads/2.0.0/:

Instance documents SHOULD always use a versioned schema URL to make clear which schema version they comply with:

Upgrade APIs MAY provide automated data migration between schema (package) versions, e.g. https://example.org/upgrade/my-package/1.0.0...2.0.0.

Standard Extensions

On top of plain JSON Schema and JSON-LD, OO-LD defines a small set of extensions. This section covers the JSON-LD extensions and the JSON Schema extensions in turn.

JSON-LD

OO-LD targets [[JSON-LD11]].

Processing mode (@version)

OO-LD composition relies on JSON-LD 1.1 features, in particular scoped contexts: a $ref within a type: object property is reflected as a property-scoped @context (see ). Such features are unavailable to a processor running in the json-ld-1.0 processing mode.

Generated OO-LD contexts SHOULD therefore declare "@version": 1.1 (the JSON number 1.1, not the string "1.1"). Modern processors default to the 1.1 processing mode, so this is a guard rather than a strict requirement: it prevents a JSON-LD 1.0 processor from silently mis-processing a 1.1 document ([[JSON-LD11]] §4.1.1). Because the first encountered @version entry determines the processing mode, it is sufficient to declare "@version": 1.1 once in the base context of a composition (for example a root Thing schema).

Multi-Mapping

JSON-LD allows only a single keyword-IRI mapping (or more precisely, ignores all but the last mapping). There is currently no way to express that a property has two IRIs (e.g. "label": {"@id": ["schema:name", "skos:prefLabel"]}, see json-ld/json-ld.org#160). As a lightweight inline workaround, an additional context notation is provided: <property>*(*) pointing to additional @id mappings, to document alternative options or drive custom RDF generation. For the general case - more than two mappings per term, mappings a subclass can add or drop, and mappings that round-trip to a mapping set - use the structured x-oold-context notation below.

The notation can also drive data transformation and normalization. For example, a dataset in which persons and organizations report their relations in a syntactically non-interoperable way can be normalized into a consistent unified dataset (see OO-LD/oold-schema#11).

Term mappings and synonyms (x-oold-context)

The * notation above documents at most a small, fixed set of alternative @ids inline and carries no metadata about each mapping. For the general case OO-LD adds the schema-level keyword x-oold-context: an object keyed by term, where each term holds a dict keyed by the synonym IRI.

In the minimal case a term simply lists alternative IRIs, each with an empty value ({}):

Here description (primarily schema:description) also maps to rdfs:comment and skos:definition. Each bare synonym IRI defaults to predicate_id: skos:exactMatch and inherits the primary term's coercion on promotion.

Each value is itself a JSON-LD term-definition fragment (@type, @container, ...) that is promotable verbatim into @context, optionally carrying a strippable x-sssom block with mapping metadata:

Each entry is one mapping:

  • the key is the synonym IRI (the SSSOM object_id);
  • the value is a JSON-LD term-definition fragment (@type, @container, ...) that is promotable as-is, plus an optional x-sssom block;
  • x-sssom is strippable metadata: predicate_id (default skos:exactMatch), optional confidence and mapping_justification. The subject_id is implicit (the term's primary @context IRI) and the object_id is the key.

This satisfies what the * notation cannot:

  • More than two mappings - the dict holds any number of synonym IRIs.
  • Ontology family - prefix-driven: the family is the IRI namespace (schema:, bfo:, emmo:), so RDF export can prioritize a preferred namespace without an explicit tag.
  • Override under composition - the dict is keyed by IRI and resolved by the merge and override model: most-derived-wins, with null removing an inherited mapping. Keys are compared as expanded IRIs, so the compact (skos:prefLabel) and full (http://www.w3.org/2004/02/skos/core#prefLabel) forms of one IRI are a single key and override correctly across mixed notations.
  • SSSOM interoperability - each entry is one SSSOM mapping row (subject_id / object_id / predicate_id / confidence / mapping_justification), so a mapping set round-trips to and from x-oold-context.

To promote to @context, an OO-LD preprocessor picks the prioritized synonym (by namespace), then { "@id": <key>, ...value without x-sssom } becomes the term's definition in the real @context; the x-sssom blocks are dropped, so standard JSON-LD tools then run on a clean context.

Framing

JSON-LD 1.1 Framing reshapes a flat or arbitrarily-structured RDF graph into a specific tree layout described by a frame. An OO-LD schema already describes exactly such a tree - its properties give the nesting, its @context gives the term IRIs, a type constant (x-oold-instance-rdf-type, or a const on the type property) gives the node type, and x-oold-range gives the type of embedded or referenced objects - so an OO-LD-aware tool MAY auto-construct a frame from the schema. Framing an instance graph with that frame produces a JSON document shaped like the schema, which then validates against the same schema.

This makes an OO-LD schema bidirectional: its @context drives expansion (JSON to RDF), and the frame derived from its structure drives framing (RDF back to the schema's JSON tree). The derivation is mechanical: the schema's class type becomes the frame @type; an inlined object property becomes a nested subframe that embeds the referenced node; a reference-valued property (including one whose term is mapped with JSON-LD @reverse) becomes a subframe with @embed: @never so its targets stay IRIs, in line with the inline-versus-reference choice x-oold-range already records; and @explicit / @requireAll / @default follow from additionalProperties and required. The frame's @context is the composition of the referenced schemas' contexts.

Framing is not the only option for this transformation. When the source data lives in a triplestore or behind a SPARQL endpoint, a SPARQL CONSTRUCT query - also derivable from the schema - can select and reshape the relevant subgraph directly, including deriving reverse relations such as employees from the inverse of schema:worksFor; compacting that result with the schema's @context (and framing it where nesting is required) yields the same OO-LD instance.

JSON Schema

OO-LD targets [[JSONSCHEMA]] (2020-12) as its normative dialect. An OO-LD schema SHOULD declare the OO-LD dialect meta-schema (which extends 2020-12) as its $schema, e.g. "$schema": "https://oo-ld.github.io/oold-schema/latest/meta/oold-meta-schema.json" - pinning a specific version (e.g. .../0.4.0/meta/oold-meta-schema.json) for reproducibility. Declaring the plain 2020-12 meta-schema (https://json-schema.org/draft/2020-12/schema) remains valid for tools that only understand standard JSON Schema.

2020-12 is REQUIRED, not merely preferred: OO-LD's composition places $ref alongside sibling keywords (e.g. a property carrying type, x-oold-range and @context, or allOf: [{$ref: ...}] next to properties). Keywords adjacent to $ref are only evaluated from JSON Schema 2019-09 onward; in Draft 4 and Draft 7 they are ignored ([[JSONSCHEMA]] §8.2.3.1). Keywords such as const (used throughout this document) are likewise only available from draft-06 onward. Migration from the earlier Draft-4-style notation: rename definitions to $defs, id to $id, and use the numeric form of exclusiveMinimum/exclusiveMaximum instead of the boolean form.

Multilanguage support

There are two distinct localization concerns: translating a schema's own annotations, and translating a value carried by an instance.

Localizing schema annotations

The JSON Schema annotation keywords title and description carry a single, default human-readable string used by tooling (for example for UI generation). To provide localized variants, OO-LD adds the keywords x-oold-multilang-title and x-oold-multilang-description. Their value MUST be an object whose keys are BCP 47 language tags (e.g. en, de, en-GB) and whose values are the translated strings. A schema SHOULD still provide a default title / description; a consumer that has no entry for the requested language falls back to that default. These keywords localize the schema's own labels and are not interpreted as JSON-LD.

Localizing instance values

To localize a value of an instance - a translatable string in the data that should round-trip to language-tagged RDF literals - do not use the keywords above; use the standard JSON-LD mechanism. There are two equivalent JSON-LD-native ways to carry such a value, both producing the same language-tagged literals.

Explicit - model the value as an object that pairs its text with its language by aliasing text to @value and lang to @language. This form is convenient for form-based editors, where each translation is an editable row:

Compact - a language map keyed directly by language tag, via @container: @language ([[JSON-LD11]] Language Maps):

Range of properties (x-oold-range)

JSON Schema itself supports linked data only in the form of a subobject; references to independent external objects are just URL-strings without further restrictions. To express constraints on the type of the referenced object - as in OWL and SHACL - the keyword x-oold-range is introduced (see also json-schema-org/json-schema-vocabularies#55). It takes one of three forms:

  1. An IRI (string) referencing a single allowed target schema. This is the common case:
  1. An array of IRIs, expressing a union of allowed target schemas, e.g. ["Organization.schema.json", "Person.schema.json"].

  2. An OO-LD subschema, the most expressive form. Unions (anyOf / oneOf), intersections (allOf) and inline constraints can be combined to describe an anonymous subclass. References to other schemas inside x-oold-range MUST use x-oold-ref, never $ref (see below). The single-IRI form (1) is a shorthand for { "allOf": [ { "x-oold-ref": "Organization.schema.json" } ] }:

A range subschema MAY also carry additional annotations (e.g. title, description or further x-oold-* keywords) to support tooling - for example a human-readable label for an autocomplete dropdown, or hints used when generating a SHACL shape.

Lexical form of the reference

The value of an IRI-valued property is a JSON string. Its role as a reference comes from the @context ("@type": "@id") and its class from x-oold-range. Its lexical form SHOULD be constrained with an IRI/URI-family format so that malformed values are rejected; the choices, from most to least permissive:

  • Any IRI reference - "format": "iri-reference". By [[RFC3987]] this accepts absolute IRIs, compact IRIs (ex:alice, schema:Person) and context-relative references alike - the forms OO-LD instances routinely use - so it is the RECOMMENDED default. It also accepts a bare term such as alice, expanded against the context's @base / @vocab.
  • Absolute IRIs only - "format": "iri". A compact IRI is itself a valid absolute IRI (scheme ex, path alice), so iri accepts ex:alice; choose it to additionally forbid relative references.
  • Stricter, ASCII only - "format": "uri" or "uri-reference", where values are known not to use internationalized (non-ASCII) IRIs.
  • Compact form specifically - a "pattern" such as "^[A-Za-z_][\\w.-]*:(?!//)\\S*$", which accepts ex:alice and schema:Person while rejecting http://…; the prefix MUST be defined in the @context.
Why x-oold-ref and not $ref

x-oold-range is a custom keyword, so a $ref placed inside it is undefined behavior for generic JSON Schema tooling ([[JSONSCHEMA]] §9.4.2). In practice the behavior is not merely undefined but inconsistent: generic reference resolvers eagerly inline such a $ref, and because x-oold-range targets can form a cyclic graph of schemas this can pull in an unbounded graph, while schema-aware bundlers instead drop it.

x-oold-ref avoids this. Generic tools only follow the standard $ref keyword, so they leave x-oold-ref untouched; OO-LD-aware tools resolve it deliberately and lazily, with cycle detection. The standard $ref continues to be used for ordinary schema composition (allOf, properties, $defs), which bundlers are expected to resolve. Because the only difference is the keyword name, the mapping is reversible: an OO-LD-aware tool can mechanically replace x-oold-ref with $ref to obtain a plain, fully-resolvable JSON Schema - the explicit opt-in to resolving the (possibly cyclic) graph.

Generation targets

x-oold-range (and the reverse properties below) exist so that the logical and conceptual modelling layers can be generated from the same OO-LD source instead of being maintained separately: a range constrains the type of a referenced object, which an OO-LD-aware tool emits as a SHACL property shape (sh:class / sh:node) and as an OWL property restriction. SHACL 1.2 (in progress, including Node Expressions for derived values) and OWL are the intended targets. These are generation targets, not additional validation performed by generic JSON Schema tools.

Reverse properties

Many relations are symmetric (e.g. Organization employs Person ⇔ Person works for Organization) and users want to edit them from both sides, without storing the information twice. The keywords x-oold-reverse-properties and x-oold-reverse-required declare such a [=reverse property=], mapped with JSON-LD @reverse in the @context. (The earlier x-oold-reverse-default-properties array is deprecated: mark a reverse property shown by default with x-oold-ui-default-property on the property itself - see - which, unlike the merged array, is overridable under composition.) To make employees the reverse of organization:

  • define employees in x-oold-reverse-properties of Organization;
  • define organization in the properties of Person;
  • map organization to a semantic property, e.g. schema:worksFor, in the @context of Person;
  • map employees with @reverse to the same property in the @context of Organization ([[JSON-LD11]] reverse properties).

An OO-LD-aware implementation uses this to read and modify properties that are actually stored in another object: loading an Organization editor prepopulates employees by querying which persons work for it; storing the Organization writes it into each referenced person's organization field; and removing a person from employees removes the organization from theirs.

UI Generation

OO-LD schemas double as the source for auto-generated forms and views. UI intent is carried in two layers:

  • Portable, renderer-agnostic keywords in the x-oold-ui-* vocabulary, defined here. Any form generator can honour them.
  • Renderer-specific keywords passed through under a vendor prefix - x-jedison-* for jedison, the successor of json-editor - for options that do not generalize.

Every keyword keeps the x- prefix, so it is a valid JSON Schema extension keyword and a valid OpenAPI 3.0 specification extension, and generic 2020-12 validators ignore it. The x-oold-ui-* keywords form their own optional dialect, described by the OO-LD UI meta-schema; the core meta-schema includes those definitions so an OO-LD schema carrying UI annotations validates in one pass.

The W3C SHACL 1.2 User Interfaces module (First Public Working Draft, 2026) describes the same concern - form and view generation - for RDF graphs. x-oold-ui-* is the portable counterpart for the physical (JSON Schema) layer: it rides on JSON Schema and therefore needs no RDF toolchain, while SHACL 1.2 UI targets a SHACL shapes graph. The two are intended to be a crosswalk rather than competing vocabularies. Candidate mappings for the x-oold-ui-* vocabulary follow (sh: = SHACL core, shui: = SHACL UI; SHACL 1.2 UI is a First Public Working Draft, so entries marked tentative may still change):

x-oold-ui-* keyword SHACL 1.2 UI candidate
x-oold-ui-property-order sh:order
x-oold-ui-property-group sh:group (a sh:PropertyGroup)
x-oold-ui-widget (and format) shui:editor / shui:viewer with a widget class (shui:AutoCompleteEditor, shui:EnumSelectEditor, shui:DatePickerEditor, shui:BooleanEditor, shui:TextFieldEditor, shui:ImageViewer, shui:ValueTableViewer, ...)
x-oold-ui-hint sh:description (help text)
x-oold-multilang-ui-hint sh:description with language-tagged strings
x-oold-ui-enum-titles rdfs:label on each sh:in value, rendered by shui:EnumSelectEditor
x-oold-multilang-ui-enum-titles language-tagged rdfs:label on each sh:in value
x-oold-ui-form-hidden suppress shui:editor (tentative)
x-oold-ui-render-hidden suppress shui:viewer (tentative)
x-oold-ui-default-property no direct counterpart; a shui:propertyRole / default-visibility convention (tentative)

The OO-LD UI meta-schema MAY itself carry a JSON-LD @context recording these term identities, so the vocabulary is self-describing.

The x-oold-ui-* vocabulary

All keywords apply to the (sub)schema of a single property.

Keyword Description
x-oold-ui-widget Widget hint for a value whose intended widget is not a registered JSON Schema format (for example table, tabs, grid, autocomplete, textarea, checkbox, markdown, color). Registered formats (date, uri, uuid, ...) stay in format. Maps to jedison x-format.
x-oold-ui-property-order Display order of this property within its object or group; lower sorts first. Maps to jedison x-categoryOrder.
x-oold-ui-property-group Name of the group, tab or category this property belongs to. Maps to jedison x-category / x-propGroup.
x-oold-ui-form-hidden Hide this property in the editing form. Maps to jedison x-hidden.
x-oold-ui-render-hidden Hide this property in the rendered (read) view.
x-oold-ui-enum-titles Human display labels for the default language, aligned positionally with enum: the Nth label is the title of the Nth enum value. For enum: ["pi", "postdoc", "phd"] the value ["Principal investigator", "Postdoc", "PhD student"] labels each option. Localize with x-oold-multilang-ui-enum-titles. Distinct from the identifier-safe code names in x-enum-varnames. Maps to jedison x-enumTitles.
x-oold-multilang-ui-enum-titles BCP-47 language map of x-oold-ui-enum-titles arrays (mirrors x-oold-multilang-title); each array aligns positionally with enum. For enum: ["pi", "postdoc", "phd"]: {"en": ["Principal investigator", "Postdoc", "PhD student"], "de": ["Projektleitung", "Postdoc", "Doktorand"]}.
x-oold-ui-hint Short help text shown with the field, in the default language. Localize with x-oold-multilang-ui-hint. Maps to jedison x-info.
x-oold-multilang-ui-hint BCP-47 language map of the x-oold-ui-hint text (mirrors x-oold-multilang-title).
x-oold-ui-default-property Whether this optional property is shown by default in generated user interfaces. Replaces the object-level defaultProperties array: a per-property boolean is overridable under composition (most-derived-wins), so a derived schema can set it false, whereas the merged array form was extend-only.
x-enum-varnames Identifier-safe code names aligned positionally with enum, for code generation. For enum: ["m", "s"] the value ["metre", "second"] names each option (so a generator can emit Unit.metre instead of Unit.m). An established vendor extension (OpenAPI Generator; NSwag uses the camelCase x-enumNames). Kept as-is; distinct from the human labels in x-oold-ui-enum-titles.
x-enum-descriptions Per-value descriptions aligned positionally with enum, the established companion of x-enum-varnames. For enum: ["m", "s"]: ["SI base unit of length", "SI base unit of time"].

The text-valued keywords have a x-oold-multilang-* variant carrying a BCP-47 language map, mirroring x-oold-multilang-title (see ): x-oold-multilang-ui-hint and x-oold-multilang-ui-enum-titles.

For enum code generation OO-LD keeps the established x-enum-varnames (identifier-safe names aligned with enum) and its companion x-enum-descriptions; these are widely supported vendor extensions (OpenAPI Generator; NSwag's x-enumNames) and are distinct from the human display labels in x-oold-ui-enum-titles.

x-oold-ui-default-property replaces the json-editor defaultProperties array (which listed the optional properties shown initially). That array is extend-only under composition: because composed schemas merge the arrays, a derived schema can add a default property but cannot switch one off. A per-property boolean is overridable - it resolves most-derived-wins (see ), so a derived schema sets it to false to hide a property a base schema showed. For the same reason x-oold-reverse-default-properties is deprecated in favour of x-oold-ui-default-property on the reverse property.

Widget hints: format vs x-oold-ui-widget

format carries the widget hint when its value is a registered JSON Schema 2020-12 format (date, date-time, time, duration, email, uri, iri, uuid, ...); a validator may check it and a form generator picks the matching input. Values that are not registered formats (table, tabs, grid, autocomplete, textarea, checkbox, markdown, color, ...) are widget-only and go in x-oold-ui-widget, leaving format for validation semantics.

Validator vs. form widget

A modern JSON Schema 2020-12 toolchain - ajv, the json-editor / jedison built-in validator, Hyperjump, Pydantic v2, OpenAPI 3.1 - honours const and keywords placed alongside $ref, so OO-LD's composition constructs validate as written; the older assumption that such tooling silently drops them does not hold for these validators. A narrower caveat concerns UI generation rather than validation: a form generator's widget rendering of a keyword may differ from what its validator enforces - for example json-editor validates const but does not necessarily render the field as a fixed value. A consumer restricted to a Draft-4-only JSON Schema processor remains the exception, since keywords adjacent to $ref and const are only guaranteed from later drafts.

Delivery: inline or overlay

UI keywords may be embedded in the schema (inline):

{
  "$schema": "https://oo-ld.github.io/oold-schema/latest/meta/oold-meta-schema.json",
  "$id": "UiAnnotations.schema.json",
  "@context": [
    {
      "@version": 1.1,
      "schema": "https://schema.org/",
      "name": "schema:name",
      "role": "schema:jobTitle",
      "homepage": { "@id": "schema:url", "@type": "@id" },
      "notes": "schema:comment",
      "internal_id": "schema:identifier"
    }
  ],
  "x-oold-uuid": "b1e7a0c2-2d4f-4a1e-9c3a-7f0e5d2b6a11",
  "title": "Researcher",
  "x-oold-multilang-title": { "en": "Researcher", "de": "Forschende Person" },
  "x-oold-iri": "schema:Person",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "title": "Name",
      "x-oold-ui-default-property": true,
      "x-oold-ui-property-order": 1,
      "x-oold-ui-property-group": "General",
      "x-oold-ui-hint": "Full name",
      "x-oold-multilang-ui-hint": { "en": "Full name", "de": "Vollständiger Name" }
    },
    "role": {
      "type": "string",
      "title": "Role",
      "enum": ["pi", "postdoc", "phd"],
      "x-enum-varnames": ["PrincipalInvestigator", "PostDoc", "PhDStudent"],
      "x-enum-descriptions": ["Leads the project", "Holds a doctorate", "Doctoral candidate"],
      "x-oold-ui-enum-titles": ["Principal investigator", "Postdoc", "PhD student"],
      "x-oold-multilang-ui-enum-titles": { "en": ["Principal investigator", "Postdoc", "PhD student"], "de": ["Projektleitung", "Postdoc", "Doktorand"] },
      "x-oold-ui-property-order": 2,
      "x-oold-ui-property-group": "General"
    },
    "homepage": {
      "type": "string",
      "title": "Homepage",
      "format": "uri",
      "x-oold-ui-property-group": "Contact"
    },
    "notes": {
      "type": "string",
      "title": "Notes",
      "x-oold-ui-widget": "markdown",
      "x-oold-ui-property-group": "Contact"
    },
    "internal_id": {
      "type": "string",
      "title": "Internal id",
      "x-oold-ui-form-hidden": true
    }
  }
}

or applied by an overlay - a separate document that patches a schema without editing it, following the OpenAPI Overlay 1.1.0 model. Overlays are a general schema-patching mechanism: the update actions can inject any keywords (semantics, constraints or presentation), and applying x-oold-ui-* annotations is one use case - useful when the schema is generated or owned elsewhere, or when one schema needs different presentation in different contexts. An overlay lists actions, each selecting nodes with an RFC 9535 JSONPath target and merging keys via update (or removing them via remove):

{
  "overlay": "1.0.0",
  "info": {
    "title": "Researcher admin overlay",
    "version": "1.0.0",
    "description": "Applies presentation-only x-oold-ui-* keywords to Researcher without editing the schema. An overlay is a general schema-patching mechanism (OpenAPI Overlay 1.1.0); UI annotation is one use case."
  },
  "extends": "./UiAnnotations.schema.json",
  "actions": [
    {
      "target": "$.properties.internal_id",
      "description": "Show the internal id for admins.",
      "update": { "x-oold-ui-form-hidden": false }
    },
    {
      "target": "$.properties.notes",
      "description": "Render notes as a plain textarea instead of markdown.",
      "update": { "x-oold-ui-widget": "textarea" }
    },
    {
      "target": "$.properties.role",
      "description": "Drop the render-time grouping for a flat admin form.",
      "remove": false,
      "update": { "x-oold-ui-property-group": "Identity" }
    }
  ]
}

The vendor keywords are documented by their respective projects: jedison's x-jedison-* (see germanbisurgi/jedison#58 and the overlay proposal #59) and, for schemas coming from OpenSemanticLab, the server-side x-osl-* keywords. Migrating a legacy OpenSemanticWorld schema to these keywords is covered in the migration guide.

Semantic delivery

An OO-LD schema carries its semantics in the top-level @context and, for the RDF type of instances, in x-oold-instance-rdf-type. How those reach a consumer depends only on which keywords the consumer tolerates. The native form is preferred; the alternatives below are mechanically generated from it, so the OO-LD document remains the single source and every form yields the same RDF.

Worked examples of each tier, including the per-class OpenAPI 3.0 mapping, are given in the guide (see Delivery to OpenAPI, MCP and LLM tooling).

Meta-schema and Vocabulary

OO-LD adds keywords on top of JSON Schema 2020-12. All OO-LD-proprietary keywords are prefixed with x-oold- so they are valid JSON Schema extension keywords and, at the same time, valid OpenAPI 3.0 Specification Extensions (OpenAPI 3.0 rejects unprefixed custom keywords in a Schema object). The only non-prefixed OO-LD-specific entry is @context, which is a JSON-LD keyword and cannot be renamed.

The OO-LD dialect is described by a meta-schema ([[OOLD-META]]). It extends the standard 2020-12 meta-schema and adds the syntax of the x-oold-* keywords, so an OO-LD schema can be validated as an OO-LD schema. The meta-schema declares its vocabularies via $vocabulary: the seven standard 2020-12 vocabularies are re-listed as required (they are not inherited through $ref, [[JSONSCHEMA]] §8.1.2.2), and the OO-LD vocabulary is declared optional (false) so that generic 2020-12 validators still process OO-LD schemas instead of refusing them.

Declaring a vocabulary does not make a validator execute keyword behavior; that is supplied by OO-LD-aware tooling (e.g. the oold library). The meta-schema only validates that x-oold-* keywords are well-formed and provides a machine-readable description for each. The x-oold-* keywords are:

Keyword Description
x-oold-context Extended term mappings (synonyms): an object keyed by term, each holding a dict keyed by synonym IRI whose value is a JSON-LD term-definition fragment plus an optional strippable x-sssom block. Supports more than two mappings per term, override under composition (most-derived-wins; null removes), prefix-driven ontology-family prioritization, and SSSOM round-trip. Promoted into @context by OO-LD-aware tooling; see the 'Term mappings and synonyms' section.
x-oold-uuid Stable UUID identifying this schema across versions and locations.
x-oold-version Semantic version of this schema.
x-oold-prior-version Identifier or version of the immediately preceding schema version.
x-oold-backward-compatible-with URI of a prior schema version this schema is backward-compatible with.
x-oold-incompatible-with URI of a prior schema version this schema is NOT compatible with.
x-oold-iri Ontology IRI (or compact IRI) denoting the class described by this schema.
x-oold-instance-rdf-type The rdf:type(s) carried by instances of this schema, as a list of IRIs (e.g. ["schema:Person"]). OO-LD tooling materializes these as @type when exporting an instance to JSON-LD / RDF.
x-oold-ref Reference to another OO-LD schema. Use x-oold-ref (not the standard $ref) for references that appear inside OO-LD custom keywords such as x-oold-range: there a plain $ref would be eagerly - and, for cyclic schema graphs, dangerously - dereferenced by generic JSON-Schema bundlers (the behaviour is undefined per Core section 9.4.2). Keep using the standard $ref for ordinary schema composition (allOf, properties, $defs), which bundlers are expected to resolve. x-oold-ref is resolved only by OO-LD-aware tools, lazily and with cycle handling.
x-oold-range Type constraint on the target of an IRI-valued property: an IRI string, an array of IRIs, or an OO-LD subschema (using x-oold-ref for references). See the 'Range of properties' section.
x-oold-multilang-title Language map of translated title values keyed by BCP-47 language code.
x-oold-multilang-description Language map of translated description values keyed by BCP-47 language code.
x-oold-reverse-properties Properties stored on the related object but editable from this side, mapped via JSON-LD @reverse.
x-oold-reverse-required Names of reverse properties that are required.
x-oold-reverse-default-properties Deprecated. Names of reverse properties shown by default in generated user interfaces. Like the object-level defaultProperties array this is extend-only under composition; prefer a per-reverse-property x-oold-ui-default-property boolean, which is overridable.

IANA Considerations

Slot File extension Media type RFC 6906 profile Description
schema *.schema.json application/oold-schema+json - Full OO-LD schema
schema *.schema.json application/oold-schema+json oold-schema#bundled Full OO-LD schema with all $ref and remote context bundled
schema *.schema.json application/oold-schema+json http://www.w3.org/ns/json-ld#context Only the JSON-LD context
schema *.schema.json application/ld+json - Only the JSON-LD context
schema *.schema.json application/schema+json - Only the JSON Schema schema
data *.data.json application/oold-schema-instance+json - Full OO-LD instance
data *.data.json application/ld+json http://www.w3.org/ns/json-ld#* Full OO-LD instance; the profiles defined in [[JSON-LD11]] IANA considerations apply
data *.data.json application/json - Only the JSON data

Security Considerations

Both the security considerations of [[JSON-LD11]] (§C) and of [[JSONSCHEMA]] (§13) apply. In particular, a consumer SHOULD NOT blindly trust the schema an instance declares for itself (a crafted instance could point at a permissive schema) and remains responsible for validating against a schema it trusts.

Index of Terms

The following defined terms are used in this specification: [=OO-LD=], [=OO-LD schema=], [=OO-LD instance=], [=remote context=], [=resolved schema=], [=range=], [=reverse property=].