Copyright © the OO-LD contributors. This document is made available under the CC0 1.0 Universal Public Domain Dedication; W3C liability, trademark and document-license rules do not apply.
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. At the instance level an OO-LD instance is itself a valid JSON-LD document whose references resolve into a navigable object graph, so stored data can be traversed as linked objects rather than as loose identifiers.
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.
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.
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 a JSON-LD remote context, so standard JSON Schema validators and JSON-LD processors work with it directly. One caveat: JSON Schema 2020-12 treats unknown keywords as annotations, but a validator that defaults to a strict mode (for example Ajv) rejects them, so it is run in non-strict mode or with the x-oold-* keywords registered.
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.
OO-LD schema documents allow to specify all information that is needed to automatically transform data between semantically equivalent but syntactically different notations.
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, an MCP tool definition, 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.
To keep that role honest, OO-LD aligns with existing standards rather than competing with them: it treats SHACL (validation, UI generation) and OWL (the conceptual layer) as generation targets, and introduces an x-oold-* keyword only where no established standard covers the need. Its relationship with a framework like LinkML is bidirectional - LinkML can itself generate OO-LD - and therefore complementary rather than exclusive.
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 ). 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.
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]].
The normative data model of OO-LD is the JSON data model shared by [[JSONSCHEMA]] and [[JSON-LD11]]. JSON ([[RFC8259]]) is the canonical serialization: a conforming OO-LD schema or instance MUST be interchangeable as JSON, and the canonical form used for identity and integrity (for example content-hashing a versioned schema) is its JSON Canonicalization Scheme ([[RFC8785]]) serialization.
A document MAY additionally be authored or served as YAML, provided it stays within the JSON-compatible subset of YAML 1.2: no tags, anchors, aliases, or merge keys; a single document; and no implicit typing beyond what JSON expresses. Within this subset - the same profile adopted by YAML-LD - a YAML document maps one-to-one onto the JSON data model and converts to the canonical JSON without loss. YAML outside this subset is NOT a conforming OO-LD serialization.
Authors using YAML should be aware that YAML comments and implicit type coercions (for example the strings NO or 1.10 read as a boolean or a truncated number by some parsers) do not survive conversion to the canonical JSON; the JSON form is authoritative. Examples in this specification are shown as JSON, with an equivalent YAML rendering available under "View as YAML".
The following terms are used throughout this specification:
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:
@context), never as a JSON-LD document. OO-LD schema documents MUST NOT be interpreted as JSON-LD documents, because that would apply the schema's own @context to the schema itself and produce incorrect triples.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 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. An embedded object reached under an object-valued property - one whose value is an object, directly (type: object) or as the items of an array (type: array), whether inlined or brought in by $ref - SHOULD be reflected as that property's scoped JSON-LD context, so its terms resolve only under that property and cannot conflict with a same-named term elsewhere. That scoped context MAY reference the embedded schema remotely (by URL) or carry its terms inline. Where the embed graph is cyclic - a value type whose scoped context transitively references itself through remote schema files - JSON-LD processors cannot resolve the recursive remote contexts (see ); breaking the cycle requires migrating the remote reference to a local (inline) context - inlining the term definitions so there is no remote hop to recurse - which MAY be flattened onto the root context as a shared vocabulary. Moving the remote reference to the root does not break the cycle; only replacing it with local definitions does. A $ref at the root level of the OO-LD schema is listed at the root of the JSON-LD context. (A scalar reference - a property whose value is an IRI string, not an embedded object - carries its target type in x-oold-range, not a $ref, and so contributes no scoped 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.
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 ([[JSONLD11-API]], Context Processing Algorithm). 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:
Type-scoped contexts when the branches are distinguished by @type. The
scoped @context is attached to the term used as the type value and is activated only for nodes carrying that @type:
{
"@context": {
"Sensor": { "@id": "ex:Sensor", "@context": { "reading": "ex:temperature" } },
"Gauge": { "@id": "ex:Gauge", "@context": { "reading": "ex:pressure" } }
}
}
Here { "@type": "Sensor", "reading": 21 } maps reading to ex:temperature, while { "@type": "Gauge", "reading": 3 } maps the same keyword to ex:pressure.
@context) and applies only within that property's value, so the same keyword can resolve differently under different parents.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.
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).
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.
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 ).
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:
@context - the schema URL, loaded as a JSON-LD remote context. This is what makes the instance a JSON-LD document.$schema - the schema URL, identifying the schema the instance is intended to validate against.Instances SHOULD use a versioned schema URL so that it is unambiguous which schema version they conform to.
$schemaIn 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:
$schema value, if present;@context, if the referenced document declares itself to be an OO-LD schema;@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.
@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.
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.
A materialized @type is an ordinary JSON-LD @type: its IRIs resolve through the @context like any term, so a class name declared as a context term ("QuantityValue": "qudt:QuantityValue") is aliased on export, carries term synonyms, and drives frame construction. x-oold-instance-rdf-type and an inline type are thus two ways to reach the same @type: the former lets an application keep the type in the schema alone (no @type duplicated on every instance) and inject it on export, the latter carries it in self-describing data. Whether a type is subject to profile aliasing follows ordinary JSON-LD @type semantics: a value written as a term (a plain string such as "QuantityValue") resolves through the @context and so is aliased per profile if that term carries synonyms, exactly as an inline type term is, whereas a value written as a full IRI or CURIE ("qudt:QuantityValue", "http://qudt.org/...") is emitted verbatim and not aliased. This holds for a materialized x-oold-instance-rdf-type and an inline type alike - the author selects term form for a profile-aliasable type, IRI form for a fixed one. Multiplicity and aliasing are orthogonal: x-oold-instance-rdf-type co-types - it lists every rdf:type an instance asserts at once (e.g. qudt:QuantityValue and schema:QuantitativeValue together) - while x-oold-context on a type term supplies the synonyms of a single type, one chosen per profile; a co-typed type may itself carry synonyms, so the two compose.
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"]).
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:
"Mainstreet 1";@id (or its id alias), e.g. { "id": "https://example.org/PlaceA" }, denoting an independent entity by IRI;{ "type": "PostalAddress", "streetAddress": "..." }, which becomes a blank node.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:
@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.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.A term MUST NOT declare @type with a datatype that JSON-LD produces by default from a native JSON value: xsd:string (from a string), xsd:boolean (from a boolean), xsd:integer (from an integer number), and xsd:double (from a fractional number). These are exactly the datatypes reconstruction converts back to native JSON values without an @type ([[JSONLD11-API]], RDF to Object Conversion; see ): the value arrives from RDF with no datatype, and a term is never selected against a conflicting or absent type mapping ([[JSONLD11-API]], Term Selection), so the value reappears under the full predicate IRI instead. Coercing to one of these is redundant and lossy - a native JSON number already round-trips as xsd:integer or xsd:double with no coercion at all, and a boolean/string likewise. This is inherent to the compaction algorithm, not a tooling limitation; such terms are left plain (no @type), and the projection to RDF still yields the correct datatype from the native JSON type ([[JSONLD11-API]], Data Round Tripping). The behaviour assumes reconstruction with native types (useNativeTypes), the mainstream default: a processor that instead keeps every literal as a typed value object would select the coerced term, but then plain native numbers and booleans no longer return as native JSON either (they come back as { "@value": ..., "@type": ... } objects), which defeats the structural model - so native-type reconstruction is assumed throughout.
Datatypes JSON-LD does not produce by default are the ones to declare with @type: the date/time family (xsd:date, xsd:dateTime, xsd:time, xsd:duration), xsd:anyURI, and every numeric refinement outside the two native ones (xsd:float, xsd:decimal, xsd:long, xsd:int, xsd:unsignedByte, ...). These stay explicit on the literal through the round-trip and compact back onto the term, but JSON has no native syntax for them, so their value is carried as a JSON string. The consequence is worth stating plainly: a JSON-native number can only ever be xsd:integer or xsd:double; any narrower or more specific numeric datatype is reached by writing the value as a string under an @type coercion (a bare JSON number under, say, @type: "xsd:float" keeps the term but comes back as its canonical string form). Numbers are therefore best left as native JSON numbers, uncoerced, unless the exact RDF datatype matters.
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.
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:
@context alone.{ "<property>": {} }. The embedded object needs an explicit type only when the property's scoped @context is type-scoped - keyed by the value's @type to distinguish several embedded types (or to stamp the node's rdf:type); a flat scoped context for a single embedded type needs none. Where required, tooling materializes the type on export (see ).type: "array") MUST declare @container: "@set" (or "@list"): without it a single-element array returns as a scalar and violates the array type. A property that also permits a scalar (an anyOf/oneOf of a literal and an array) MAY declare it for a stable array shape, but need not - the scalar form still validates, and a single value and a one-element array are JSON-LD-equivalent. Round-trip equality is set equality; use @list only where order is significant, at the cost of merge and query ergonomics.@language (or @container: "@language") so multilingual instance values round-trip - distinct from x-oold-multilang-title / x-oold-multilang-description, which localize the schema's own annotations, not instance data.A datatype-coerced value round-trips by RDF value, not byte-for-byte. A JSON number or boolean returns as a native JSON number or boolean (recovered from the typed literal - so 40.75 returns as the number 40.75, not its "4.075E1" lexical form), which a reconstruction tool obtains by reading RDF literals with native types (jsonld.fromRDF({ useNativeTypes: true })). Other datatypes - dates, durations, and any non-native xsd: type - return as their canonical lexical string (e.g. a normalized xsd:date). Round-trip equality for typed literals is value equality after datatype canonicalization.
An embedded object is mapped by a scoped @context on its property (referencing the embedded type's own context). These scoped contexts form an embed graph between schemas, and that graph SHOULD be acyclic: model a property whose value is an independent entity, or whose type would close an embed cycle (a type embedding itself, or two types embedding each other), as a reference - @type: "@id" plus x-oold-range, with no scoped @context - rather than an embed. This is the linked-data analog of using a pointer instead of inlining a recursive data structure. A self-reference through the top-level @context (a property that nests the same type but carries no scoped context, so the global context maps the nested keys - e.g. a Process with sub-Processes) is not part of this graph and round-trips normally, bounded by the instance's actual depth.
A JSON-LD processor validates a term's scoped @context when it processes the containing context ([[JSONLD11-API]], Create Term Definition), before any data is seen. The specification bounds this for recursive scoped contexts (the validate scoped context flag, the remote-context set, and a context overflow error), but the mainstream processors do not honor that bound: jsonld.js (8.3.3) exhausts the heap and PyLD (3.1.0) raises a recursion error on a cyclic scoped-context web. So although such a context is valid per the specification, it cannot be round-tripped by current tooling - keep the embed graph acyclic and use references for cyclic edges (see Range of properties).
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.
x-sssom)A schema states which external ontology resources it corresponds to with a top-level x-sssom block - the term-synonym mapping row lifted to the schema level. Its subject is the schema itself, identified by its $id: in OO-LD the schema document at that URL is the class definition, so $id serves as the mapping's subject_id. It is an object keyed by the object IRI of a resolvable resource (an RDF/OWL class, a controlled-vocabulary term, another schema), each value carrying the SSSOM slots: predicate_id (default skos:exactMatch; skos:closeMatch for a looser correspondence), mapping_set_id, and the rest of the open bag. Because these are SKOS mapping predicates the correspondence is non-logical and reasoner-safe: skos:exactMatch asserts interchangeability, not the logical owl:equivalentClass (a schema that wants the reasoner-affecting claim uses owl:equivalentClass explicitly as the predicate_id). It is an annotation about the schema itself and is not stamped onto instances - distinct from x-oold-instance-rdf-type, the rdf:types instances carry on export (see ), which are stamped onto instance data.
Here the schema document is fetched from the $id URL, it is declared an exact match for schema:Person, and instances exported to RDF carry @type: schema:Person. Commonly a schema's exact match and the entries in x-oold-instance-rdf-type resolve to the same IRI, but they may differ - a schema may model a more specific subclass inline while emitting a broader rdf:type, or exact-match one class while close-matching a related one in another vocabulary. Because this is the same mapping machinery as term synonyms, the schema-level mappings carry the same predicate_id semantics, mapping_set_id selection and SSSOM round-trip (see ).
OO-LD-aware tooling uses these correspondences 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.
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:
https://example.org/b5203131-7321-46bb-8a11-acb3d1015840.schema.json/1.1.0.https://example.org/my-package/2.0.0/b5203131-7321-46bb-8a11-acb3d1015840.schema.json.https://raw.githubusercontent.com/MyOrg/my-package/refs/heads/2.0.0/b5203131-7321-46bb-8a11-acb3d1015840.schema.json.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.
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.
OO-LD targets [[JSON-LD11]].
@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).
x-oold-context)A JSON-LD @context binds each term to a single IRI; a term given several @ids keeps only the last. Real vocabularies overlap, so one structural term routinely corresponds to several ontology IRIs - a consensus mapping plus community alternatives. OO-LD expresses these with the schema-level keyword x-oold-context: an object keyed by term, where each term holds a dict keyed by the synonym IRI, and each synonym is a promotable JSON-LD term-definition fragment carrying optional mapping metadata. A term here is any context term - most often a property, but equally a class (through the type term, see ) or an individual (a value term under @vocab coercion, see ) - so the same machinery aliases properties, classes and individuals alike.
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 promotable JSON-LD term-definition fragment (@type, @container, ...) plus an optional x-sssom block, and the subject_id is implicit - the term's primary @context IRI.
Processing contract. A conforming OO-LD mapping processor reads exactly four things from each entry: the synonym IRI (the key), the promotable term-definition fragment, and two x-sssom slots - predicate_id and mapping_set_id. Everything else an entry carries (the rest of x-sssom, any further fragment keys) is preserved but not interpreted. Those two slots, over the SKOS predicate vocabulary, are the whole stable contract an implementation depends on.
predicate_id is a SKOS mapping predicate - skos:exactMatch (the default when the slot is absent), skos:closeMatch, skos:broadMatch, skos:narrowMatch or skos:relatedMatch - relating the term's primary IRI (subject) to the synonym IRI (object); it decides which entries denote equivalence. It is written as a full IRI or a CURIE and compared by expansion to an absolute IRI, the same rule the synonym keys follow, so skos:exactMatch and http://www.w3.org/2004/02/skos/core#exactMatch are one predicate. x-oold-context is a schema-level keyword consumed by OO-LD processors (it is promoted into a clean @context before any generic JSON-LD processor runs), so its CURIEs - the synonym keys and the predicate_id / mapping_set_id values alike - are expanded not against the instance @context but against a fixed well-known prefix set the meta-schema defines (skos, rdfs, owl, xsd, sssom), reached through the schema's $schema. The contract therefore holds without the author redeclaring those prefixes in the data context; a bare local name (exactMatch) is not a valid predicate_id.
mapping_set_id names the mapping set(s) an entry belongs to, for profile-based selection. SSSOM defines mapping_set_id at the set level; OO-LD records it inline on the entry, and an entry MAY belong to several sets (a mapping can appear in more than one), so its value is one iri-reference or an array of them, compared by expansion as predicate_id is.
Selection. To promote x-oold-context into a real @context, a preprocessor selects one synonym per term for a target profile, writes { "@id": <synonym IRI>, ...fragment without x-sssom } as that term's definition, and drops the x-sssom blocks, so standard JSON-LD tools then run on a clean context. A profile is expressed either as an ordered list of IRI namespaces (ontology-family priority - schema: before bfo: before emmo:) or as one or more mapping_set_ids (a set may span namespaces, e.g. a PMDco profile of pmd: plus reused obo: terms). A term with no synonym matching the target keeps its default @context IRI: selection never borrows another profile's synonym.
Co-emission. Selection yields one IRI per term; for interoperability a converter MAY additionally co-emit the instance value under other synonyms' IRIs. This is a pragmatic interoperability aid, not a logical entailment: skos:exactMatch records that two terms are interchangeable across a wide range of applications, but it is not owl:equivalentProperty / owl:equivalentClass and licenses no reasoner inference - which is exactly why the mapping predicates are SKOS (reasoner-safe) rather than OWL. By default a converter co-emits only skos:exactMatch entries; entries whose predicate_id is skos:closeMatch/broadMatch/narrowMatch/relatedMatch SHOULD NOT be co-emitted unless a consumer explicitly requests it, since such a triple asserts a broader, narrower or merely related relation, not that the value holds under the synonym property, so the requester takes responsibility for that reading.
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 - and predicate_id / mapping_set_id values - 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 round-trip. Each entry is one SSSOM mapping row: subject_id implicit, object_id the synonym IRI, and predicate_id / mapping_set_id / confidence / mapping_justification / object_source* / ... the SSSOM slots defined by the SSSOM LinkML schema at https://w3id.org/sssom/. Entries sharing a mapping_set_id reconstitute one SSSOM mapping set; the propagatable object_source / object_source_version slots pin each mapping to the ontology version it was validated against and, like the rest of the context, propagate from a parent schema's declaration to inherited mappings under composition. SSSOM is the RECOMMENDED carrier for this metadata, not a normative dependency: OO-LD fixes only the SKOS meaning of predicate_id and the selection role of mapping_set_id, and the remaining slots ride along untouched, so SSSOM may evolve without affecting OO-LD. A bare SKOS predicate with no metadata, or a richer carrier (statement-level RDF-star, PROV for provenance), serves equally where SSSOM is not wanted.
Conversely, several context terms mapping to one IRI normalize syntactically non-interoperable input keys onto a single predicate - the complement of one term carrying several IRIs. Combined with @reverse and framing, this turns a dataset whose records report the same relation in different ways into a consistent unified graph.
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.
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.org/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.
There are two distinct localization concerns: translating a schema's own annotations, and translating a value carried by an instance.
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.
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):
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:
An array of IRIs, expressing a union of allowed target schemas, e.g. ["Organization.schema.json", "Person.schema.json"].
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.
An x-oold-range value is a reference: the property holds the target's IRI, and an OO-LD-aware loader MAY dereference that IRI to obtain the target document itself, so a large or shared object can live in a separate document and be pulled in on demand (data bundling). A dereferenced target MUST validate against the property's declared range. This holds whether the reference is written as a bare IRI string or as a { "@id": … } object; generic tooling leaves it unresolved, exactly as it leaves x-oold-ref (see ).
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:
"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."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."format": "uri" or "uri-reference", where values are known not to use internationalized (non-ASCII) IRIs."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.@vocab)The synonym machinery keys x-oold-context by term, so it reaches property and class terms but not IRIs that appear as instance values - for example the unit IRIs a quantity property points at, where two widely used unit vocabularies name the same unit differently: QUDT http://qudt.org/vocab/unit/SEC and the Ontology of units of Measure http://www.ontology-of-units-of-measure.org/resource/om-2/second. Coercing the property with "@type": "@vocab" (rather than "@type": "@id") closes the gap: a string value is then resolved against the active context's terms before the base IRI, so declaring the unit as a value term ("second": "http://qudt.org/vocab/unit/SEC") lets an instance write the readable "second" while the same x-oold-context synonyms and profile selection alias it ("x-oold-context": { "second": { "om:second": { … } } }); full IRIs remain valid values. Individual mappings round-trip through SSSOM like any other.
Because @vocab expands an unmatched string against the vocabulary - concatenating it onto the default vocabulary base when one is set (minting a new IRI), or leaving it a relative IRI when none is - a typo silently becomes a stray IRI rather than an error. A property coerced "@type": "@vocab" therefore SHOULD constrain its values with an enum of the value terms (optionally named with x-enum-varnames) or with x-oold-range, so only intended individuals are accepted. The value terms SHOULD also be kept from colliding with JSON-LD keyword aliases (id, type) or other context terms, since a value term shares the context's global term namespace - a term added for a value would otherwise also rewrite a property or keyword of the same name. Confining the value terms to the property's own scoped @context keeps them out of that shared namespace, since they then resolve only for that property's values; naming them with opaque identifiers such as UUIDs avoids the clash where readability is not required.
x-oold-ref and not $refx-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.
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.
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 works_for:
works_for in the properties of Person, mapped to a semantic property (schema:worksFor) in the @context of Person;employees in x-oold-reverse-properties of Organization, mapped 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 works_for field; and removing a person from employees removes the organization from theirs.
OO-LD schemas double as the source for auto-generated forms and views. UI intent is carried in two layers:
x-oold-ui-* vocabulary, defined here. Any form generator can honour them.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. Per JSON Schema 2020-12, unknown keywords are annotations and are ignored for validation; a validator that defaults to a stricter mode (for example Ajv's strict) may reject them, so it is run in non-strict mode or with the keywords registered. 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.
x-oold-ui-* vocabularyAll 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.
format vs x-oold-ui-widgetformat 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.
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.
UI keywords may be embedded in the schema (inline):
{
"$schema": "https://oo-ld.org/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-sssom": { "schema:Person": { "predicate_id": "skos:exactMatch" } },
"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
}
}
}
$schema: https://oo-ld.org/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-sssom:
schema:Person:
predicate_id: skos:exactMatch
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" }
}
]
}
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.
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.
@context - Model Context Protocol tool schemas (inputSchema / outputSchema) as well as LLM tool-use and structured-output APIs, which carry the context through and can use it as grounding.@context), the context and type SHOULD be delivered per class as x-jsonld-context and x-jsonld-type following REST API Linked Data Keywords: @context maps to x-jsonld-context and x-oold-instance-rdf-type to x-jsonld-type. That draft requires references inside these keywords not to be dereferenced automatically, consistent with the x-oold-ref rule (see ). The mapping is reversible, so such an export can be read back into an OO-LD schema.title / description annotations the model also reads. Such a subset typically also constrains the JSON Schema itself (closed objects, all-required properties, limited composition), so the delivery step may apply a small structural transform - for example flattening an allOf inheritance chain - in addition to folding the IRIs; the semantics still originate in the single OO-LD source.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).
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. This governs dialect selection through $vocabulary (a validator that does not implement an optional vocabulary continues rather than refusing the schema); it is distinct from a validator's own strict mode, which may still reject the unknown x-oold-* keywords unless run non-strict or with them registered (see ).
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 (a property, class, or value 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. OO-LD tooling reads only two x-sssom slots - predicate_id (a SKOS mapping predicate) and mapping_set_id (for profile-based selection); all other slots ride along and round-trip to SSSOM. Supports override under composition (most-derived-wins; null removes) and namespace/mapping-set selection. 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-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. |
| 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 |
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.
The following defined terms are used in this specification: [=OO-LD=], [=OO-LD schema=], [=OO-LD instance=], [=remote context=], [=resolved schema=], [=range=], [=reverse property=].