Repository navigation
Expand file tree
/
Copy pathindex.html
More file actions
1777 lines (1697 loc) · 188 KB
/
Copy pathindex.html
File metadata and controls
1777 lines (1697 loc) · 188 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<!-- DO NOT EDIT - generated by scripts/render_spec.py from spec/. Edit the Markdown source and run `make spec`. -->
<html lang="en">
<head>
<meta charset="utf-8">
<title>OO-LD Schema</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://www.w3.org/Tools/respec/respec-w3c" class="remove" defer></script>
<script class="remove">
// ReSpec configuration. See https://respec.org/docs/ for all options.
var respecConfig = {
"specStatus": "base",
"shortName": "oold",
"editors": [
{
"name": "Simon Stier",
"company": "OO-LD",
"companyURL": "https://github.com/OO-LD"
},
{
"name": "Lukas Gold",
"company": "OO-LD",
"companyURL": "https://github.com/OO-LD"
},
{
"name": "Andreas Räder",
"company": "OO-LD",
"companyURL": "https://github.com/OO-LD"
}
],
"github": {
"repoURL": "https://github.com/OO-LD/oold-schema",
"branch": "main"
},
"latestVersion": "https://oo-ld.org/latest/spec/",
"edDraftURI": "https://oo-ld.org/dev/spec/",
"logos": [
{
"src": "../assets/OO-LD_logo.jpg",
"alt": "OO-LD logo",
"height": 80,
"url": "https://oo-ld.org/"
}
],
"lint": {
"no-http-props": false
},
"localBiblio": {
"JSONSCHEMA": {
"title": "JSON Schema: A Media Type for Describing JSON Documents (2020-12)",
"href": "https://json-schema.org/draft/2020-12/json-schema-core",
"publisher": "JSON Schema Organization",
"status": "Internet-Draft"
},
"JSON-LD11": {
"title": "JSON-LD 1.1",
"href": "https://www.w3.org/TR/json-ld11/",
"authors": [
"Gregg Kellogg",
"Pierre-Antoine Champin",
"Dave Longley"
],
"date": "16 July 2020",
"publisher": "W3C",
"status": "W3C Recommendation"
},
"JSONLD11-API": {
"title": "JSON-LD 1.1 Processing Algorithms and API",
"href": "https://www.w3.org/TR/json-ld11-api/",
"authors": [
"Gregg Kellogg",
"Dave Longley",
"Pierre-Antoine Champin"
],
"date": "16 July 2020",
"publisher": "W3C",
"status": "W3C Recommendation"
},
"RFC2119": {
"title": "Key words for use in RFCs to Indicate Requirement Levels",
"href": "https://www.rfc-editor.org/rfc/rfc2119",
"authors": [
"S. Bradner"
],
"date": "March 1997",
"publisher": "IETF",
"status": "Best Current Practice"
},
"RFC8259": {
"title": "The JavaScript Object Notation (JSON) Data Interchange Format",
"href": "https://www.rfc-editor.org/rfc/rfc8259",
"authors": [
"T. Bray"
],
"date": "December 2017",
"publisher": "IETF",
"status": "Internet Standard"
},
"RFC8785": {
"title": "JSON Canonicalization Scheme (JCS)",
"href": "https://www.rfc-editor.org/rfc/rfc8785",
"authors": [
"A. Rundgren",
"B. Jordan",
"S. Erdtman"
],
"date": "June 2020",
"publisher": "IETF",
"status": "Informational"
},
"RFC3986": {
"title": "Uniform Resource Identifier (URI): Generic Syntax",
"href": "https://www.rfc-editor.org/rfc/rfc3986",
"authors": [
"T. Berners-Lee",
"R. Fielding",
"L. Masinter"
],
"date": "January 2005",
"publisher": "IETF",
"status": "Internet Standard"
},
"RFC3987": {
"title": "Internationalized Resource Identifiers (IRIs)",
"href": "https://www.rfc-editor.org/rfc/rfc3987",
"authors": [
"M. Duerst",
"M. Suignard"
],
"date": "January 2005",
"publisher": "IETF",
"status": "Proposed Standard"
},
"RFC6906": {
"title": "The 'profile' Link Relation Type",
"href": "https://www.rfc-editor.org/rfc/rfc6906",
"authors": [
"E. Wilde"
],
"date": "March 2013",
"publisher": "IETF",
"status": "Informational"
},
"RFC7396": {
"title": "JSON Merge Patch",
"href": "https://www.rfc-editor.org/rfc/rfc7396",
"authors": [
"P. Hoffman",
"J. Snell"
],
"date": "October 2014",
"publisher": "IETF",
"status": "Proposed Standard"
},
"OOLD-META": {
"title": "OO-LD Meta-Schema",
"href": "https://github.com/OO-LD/oold-schema/blob/main/meta/oold-meta-schema.json",
"publisher": "OO-LD"
}
},
"subtitle": "Version v1.0.1"
};
</script>
<script type="module">
// ReSpec has no native Mermaid support, so we render `pre.mermaid` blocks
// ourselves - after ReSpec has finished rearranging the DOM.
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
mermaid.initialize({ startOnLoad: false, theme: "neutral" });
const runMermaid = () => mermaid.run({ querySelector: ".mermaid" }).catch(e => console.error("mermaid", e));
const schedule = () => setTimeout(runMermaid, 500); // let ReSpec finish rearranging first
if (document.readyState === "complete") schedule(); else window.addEventListener("load", schedule);
</script>
<style>
/* Colours here are stated so that they work in both of ReSpec's colour schemes.
ReSpec switches on `prefers-color-scheme` and, independently, on its own
colour-scheme control, so a rule keyed to the media query alone still breaks for a
reader who flips that control. Translucent tints over whatever background is in
effect, and `inherit` / `currentColor` for foregrounds, need neither selector.
Opaque light values here previously left every one of these below 2.5:1 against
dark-mode text (see OO-LD/oold-schema#166). */
.ex-tabs { margin: 1em 0; }
.ex-tablist { display: flex; gap: .25rem; border-bottom: 1px solid rgba(128, 128, 128, .35); margin-bottom: .5em; }
.ex-tab { border: 0; background: none; padding: .3em .8em; cursor: pointer; font: inherit; color: inherit; opacity: .65; border-bottom: 2px solid transparent; }
.ex-tab[aria-selected="true"] { opacity: 1; border-bottom-color: currentColor; font-weight: 600; }
.ex-panel[hidden] { display: none; }
/* The specification text must look exactly as it does today when idle: no underline,
colour, background or font change. The affordance appears only on hover, on keyboard
focus, and when the sentence is the current #fragment. */
.rule { cursor: pointer; }
.rule:hover { background: rgba(128, 160, 200, .16); }
.rule:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
/* The brand amber, carried at an alpha that keeps the sentence readable either way:
18:1 against black on white, 6:1 against light text on a dark background. */
.rule:target { background: rgba(255, 177, 32, .25); scroll-margin-top: 3rem; }
</style>
<script>
// Toggle the JSON / YAML example tabs (event delegation; runs regardless of ReSpec).
document.addEventListener("click", function (e) {
var tab = e.target.closest(".ex-tab");
if (!tab) return;
var group = tab.closest(".ex-tabs");
group.querySelectorAll(".ex-tab").forEach(function (b) {
b.setAttribute("aria-selected", b === tab ? "true" : "false");
});
group.querySelectorAll(".ex-panel").forEach(function (p) {
p.hidden = (p.id !== tab.dataset.panel);
});
});
// Deep-link a marked rule sentence. It cannot be wrapped in an <a> - a rule sentence can
// contain real links (cross-references, external refs) and an <a> cannot nest inside
// another <a> - so a click (or Enter/Space while it has keyboard focus, since it carries
// tabindex="0") sets the URL fragment instead.
document.addEventListener("click", function (event) {
var rule = event.target.closest(".rule");
if (!rule) return;
if (event.target.closest("a")) return; // a real link inside the sentence wins
location.hash = rule.id;
});
document.addEventListener("keydown", function (event) {
if (event.key !== "Enter" && event.key !== " ") return;
var rule = event.target.closest(".rule");
if (!rule) return;
event.preventDefault();
location.hash = rule.id;
});
</script>
</head>
<body>
<p class="copyright">Copyright © the OO-LD contributors. This document is made available under the <a rel="license" href="https://creativecommons.org/publicdomain/zero/1.0/">CC0 1.0 Universal</a> Public Domain Dedication; W3C liability, trademark and document-license rules do <strong>not</strong> apply.</p>
<section id="abstract">
<p><dfn data-lt="OO-LD">Object-Oriented Linked Data (OO-LD)</dfn> 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.</p>
</section>
<section id="sotd">
<p>This is a community specification produced by the <a href="https://github.com/OO-LD">OO-LD</a> project. It is <em>not</em> a W3C Standard nor is it on the W3C Standards Track. Feedback and issues are welcome in the <a href="https://github.com/OO-LD/oold-schema/issues">issue tracker</a>.</p>
<p>From version 1.0.0 the specification carries a stability commitment, and it is a narrow one. Every normative statement has an identifier that is permanent: an identifier is never reused, never silently repointed at a different requirement, and a requirement that is withdrawn is marked deprecated rather than deleted. The text of each statement is hashed, so a reword that changes what it demands cannot pass unnoticed. A 1.x release may add requirements, tighten prose that was ambiguous, and correct defects; it does not remove an identifier or change what an existing one means. A change that would do either belongs in a new major version.</p>
<p>The commitment does not extend to everything a reader might hope. Sections marked informative carry no obligations. Which requirements an implementation is held to, and how conformance is demonstrated, is the subject of a conformance programme that does not yet exist; the <a href="../rules/">rule catalogue</a> marks the statements that bind an implementation, and the gap between them and what any implementation currently does is real.</p>
<p>A companion, example-driven <a href="../">guide (primer)</a> introduces the same concepts in a didactic, step-by-step form. This document is the normative specification.</p>
</section>
<section id="introduction" class="informative"><h2>Introduction</h2><p>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 <a href="https://json-schema.org/">JSON Schema</a> and a <a href="https://json-ld.org/">JSON-LD</a> context in the same document.</p>
<p>What distinguishes OO-LD from other approaches that unify structure and semantics is that the artefact <em>is</em> 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 <code>oold</code> library) resolves the IRIs it references into linked objects, so a stored graph can be navigated as objects rather than as loose identifiers.</p>
<ul>
<li>OO-LD schema documents are supported by a wide range of existing tools (all JSON Schema and JSON-LD tooling).</li>
<li>OO-LD schema documents themselves follow linked data principles to make them retrievable over the web, allowing flexible schema compositions.</li>
<li>OO-LD schemas allow generic ex- and import of RDF.</li>
<li>OO-LD schemas are compatible with LLM APIs.</li>
<li>OO-LD schemas can be used as function (dataclass) and API signatures (OpenAPI).</li>
<li>OO-LD schemas can be used to define graphical user interfaces, in particular forms.</li>
</ul><section id="design-goals"><h3>Design Goals and Rationale</h3><section id="compatibility"><h4>Compatibility</h4><p>An OO-LD document is always a valid JSON document. This ensures that all the standard JSON libraries work seamlessly with OO-LD documents.</p>
<p>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.</p>
<p>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 <code>x-oold-*</code> keywords registered.</p></section><section id="expressiveness"><h4>Expressiveness</h4><p>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.</p>
<p>In addition, syntactical and semantic definitions can also be applied to external JSON instance documents that reference OO-LD schema documents.</p>
<p>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.</p></section><section id="interoperability"><h4>Interoperability</h4><p>OO-LD schema documents allow to specify all information that is needed to automatically transform data between semantically equivalent but syntactically different notations.</p></section></section><section id="positioning" class="informative"><h3>Positioning</h3><p>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 <code>@context</code> together with keywords such as <code>x-oold-range</code> (see <a href="#extensions"></a>) - to generate the logical layer (SHACL shapes) and the conceptual layer (OWL) from the same source, so the layers need not be maintained separately.</p>
<p>OO-LD does not aim to replace established modelling frameworks. Frameworks such as <a href="https://linkml.io/">LinkML</a>, <a href="https://spdx.github.io/spdx-spec/">SPDX 3.0</a> and <a href="https://www.spruceid.dev/treeldr/treeldr-overview">TreeLDR</a> 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 <code>@context</code> - OpenAPI 3.0, an MCP tool definition, or a strict LLM structured-output subset - is served a generated form (<code>x-jsonld-context</code> / <code>x-jsonld-type</code>, or annotations folded into <code>title</code> / <code>description</code>; see <a href="#extensions"></a>) 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.</p>
<p>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 <em>targets</em>, and introduces an <code>x-oold-*</code> keyword only where no established standard covers the need. Its relationship with a framework like <a href="https://linkml.io/">LinkML</a> is bidirectional - LinkML can itself generate OO-LD - and therefore complementary rather than exclusive.</p></section><section id="structure-first" class="informative"><h3>Structure first, semantics deferred</h3><p>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 <em>when</em> 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.</p>
<p>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 <a href="#synonyms"></a>). An unmapped term produces no triples under JSON-LD expansion; to prevent such terms from being dropped silently, an author MAY declare <code>@vocab</code> in the <code>@context</code>, 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 <a href="#range-of-properties"></a>), 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.</p></section></section>
<section id="conformance"><h2>Conformance</h2><p>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.</p>
<p>The key words <em class="rfc2119">MUST</em>, <em class="rfc2119">MUST NOT</em>, <em class="rfc2119">REQUIRED</em>, <em class="rfc2119">SHALL</em>, <em class="rfc2119">SHALL NOT</em>, <em class="rfc2119">SHOULD</em>, <em class="rfc2119">SHOULD NOT</em>, <em class="rfc2119">RECOMMENDED</em>, <em class="rfc2119">MAY</em>, and <em class="rfc2119">OPTIONAL</em> in this document are to be interpreted as described in [[!RFC2119]].</p><section id="notation"><h3>Notation</h3><p>The normative data model of OO-LD is the JSON data model shared by [[JSONSCHEMA]] and [[JSON-LD11]]. <span class="rule" id="OOLD-CNF-1120" tabindex="0" role="link" title="OOLD-CNF-1120 - A conforming schema or instance must be interchangeable as JSON, canonicalized per RFC 8785.">JSON ([[RFC8259]]) is the canonical serialization: a conforming OO-LD schema or instance <em class="rfc2119">MUST</em> 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.</span></p>
<p>A document <em class="rfc2119">MAY</em> additionally be authored or served as YAML, provided it stays within the JSON-compatible subset of <a href="https://yaml.org/spec/1.2.2/">YAML 1.2</a>: no tags, anchors, aliases, or merge keys; a single document; and no implicit typing beyond what JSON expresses. Within this subset - which coincides with the Basic profile of <a href="https://github.com/w3c/yaml-ld">YAML-LD</a> - a YAML document maps one-to-one onto the JSON data model and converts to the canonical JSON without loss. <span class="rule" id="OOLD-CNF-22d3" tabindex="0" role="link" title="OOLD-CNF-22d3 - A YAML serialization outside the JSON-compatible subset is not a conforming OO-LD serialization.">A YAML document outside this subset, including one relying on the features YAML-LD admits only in its Extended profile, <em class="rfc2119">MUST NOT</em> be treated as a conforming OO-LD serialization.</span></p>
<p>Authors using YAML should be aware that YAML comments and implicit type coercions (for example the strings <code>NO</code> or <code>1.10</code> read as a boolean or a truncated number by some parsers) do not survive conversion to the canonical JSON. <span class="rule" id="OOLD-CNF-d71d" tabindex="0" role="link" title="OOLD-CNF-d71d - Where a YAML form and its canonical JSON disagree, the JSON form is authoritative.">Where the two forms disagree, the JSON form <em class="rfc2119">MUST</em> be treated as authoritative.</span> Examples in this specification are shown as JSON, with an equivalent YAML rendering available under "View as YAML".</p></section></section>
<section id="terminology">
<h2>Terminology</h2>
<p>The following terms are used throughout this specification:</p>
<dl>
<dt><dfn data-lt="OO-LD schema|schema document">OO-LD schema</dfn></dt>
<dd>A document that is simultaneously a valid JSON Schema and a reference-able JSON-LD remote context.</dd>
<dt><dfn data-lt="OO-LD instance|instance document|instance">OO-LD instance</dfn></dt>
<dd>A JSON document that conforms to an OO-LD schema and is itself a valid JSON-LD document.</dd>
<dt><dfn data-lt="remote context|referenced context|external context">remote context</dfn></dt>
<dd>A JSON-LD context referenced by URL, as defined in [[JSON-LD11]] §3.1. The name is under discussion upstream: <a href="https://github.com/w3c/json-ld-syntax/issues/487">w3c/json-ld-syntax#487</a> proposes avoiding <em>remote</em>, because referencing a context by URL implies no network request - it may be resolved from a cache or a well-known registry, and the same holds for the <code>$ref</code> half of an OO-LD document. No replacement is settled: that issue suggests <em>external context</em>, while <em>referenced context</em> has been put to us directly. This specification keeps <em>remote context</em>, the term its normative reference defines, and will follow [[JSON-LD11]] once it changes. Both candidates resolve here so either finds this entry.</dd>
<dt><dfn data-lt="resolved schema|resolved view">resolved schema</dfn></dt>
<dd>A single, merged view of a composed schema (see <a href="#merge-and-override-model"></a>).</dd>
<dt><dfn data-lt="range">range</dfn></dt>
<dd>A type constraint on an IRI-valued property (see <a href="#range-of-properties"></a>).</dd>
<dt><dfn data-lt="reverse property">reverse property</dfn></dt>
<dd>A property that is stored on and edited from both ends of a relation (see <a href="#reverse-properties"></a>).</dd>
</dl>
</section>
<section id="basic-concepts"><h2>Basic Concepts</h2><p>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 (<em>not</em> 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).</p>
<aside class="example" title="A minimal OO-LD schema"><div class="ex-tabs">
<div class="ex-tablist" role="tablist"><button class="ex-tab" role="tab" aria-selected="true" data-panel="ex1j">JSON</button><button class="ex-tab" role="tab" aria-selected="false" data-panel="ex1y">View as YAML</button></div>
<div class="ex-panel" id="ex1j" role="tabpanel">
<pre><code class="language-json">{
"$schema": "https://oo-ld.org/latest/meta/oold-meta-schema.json",
"$id": "Minimal.schema.json",
"@context": {
"schema": "http://schema.org/",
"name": "schema:name"
},
"title": "Minimal",
"type": "object",
"properties": {
"name": { "type": "string", "description": "Name of the thing" }
}
}
</code></pre>
</div>
<div class="ex-panel" id="ex1y" role="tabpanel" hidden>
<pre><code class="language-yaml">$schema: https://oo-ld.org/latest/meta/oold-meta-schema.json
$id: Minimal.schema.json
'@context':
schema: http://schema.org/
name: schema:name
title: Minimal
type: object
properties:
name:
type: string
description: Name of the thing
</code></pre>
</div>
</div></aside>
<p>There is an asymmetry between how schemas and instances are consumed:</p>
<ul>
<li>An [=OO-LD schema=] is consumed as a JSON-LD [=remote context=] (referenced by its URL from an instance's <code>@context</code>), never as a JSON-LD document. <span class="rule" id="OOLD-SCH-a9ee" tabindex="0" role="link" title="OOLD-SCH-a9ee - An OO-LD schema document must not be interpreted as a JSON-LD document.">OO-LD schema documents <em class="rfc2119">MUST NOT</em> be interpreted as JSON-LD documents, because that would apply the schema's own <code>@context</code> to the schema itself and produce incorrect triples.</span></li>
<li>An [=OO-LD instance=] <em>is</em> a valid JSON-LD document and is processed as such.</li>
</ul>
<p>Because a schema is always reference-able as a [=remote context=], the context half is not optional. <span class="rule" id="OOLD-SCH-96a3" tabindex="0" role="link" title="OOLD-SCH-96a3 - An OO-LD schema document must carry a root @context.">An [=OO-LD schema=] document <em class="rfc2119">MUST</em> carry an <code>@context</code> at its root.</span> Without one it is not reference-able: [[JSONLD11-API]] Context Processing aborts with <code>invalid remote context</code> when a referenced document has no top-level <code>@context</code> entry, so a context-less schema would pass schema validation and then fail inside a consumer's JSON-LD processor instead. The obligation is document-level; a nested subschema under <code>properties</code> or <code>$defs</code> carries no <code>@context</code> of its own, exactly as it carries no <code>$id</code>. The requirement is presence, not content: an empty context object satisfies it, as does one whose only entry is a reference to a remote context. A schema <em class="rfc2119">MAY</em> leave every term unmapped and add semantics incrementally, as above; what it cannot do is omit the entry, because that is the one thing that makes the document unusable as a [=remote context=].</p>
<p>This asymmetry is what lets a single document serve both as a JSON Schema <code>$ref</code> target and as a JSON-LD remote <code>@context</code> for the same resource. Concretely: an instance is processed directly as a JSON-LD document (e.g. <code>jsonld.toRDF(instance)</code>), which loads the schema as a remote context via the instance's <code>@context</code>; a schema is only ever referenced as that context and, as required above, is not itself expanded as a document.</p>
<p>A term of an OO-LD schema <em class="rfc2119">MAY</em> be left unmapped. An unmapped term is structurally valid and simply produces no triples under JSON-LD expansion, which is what lets semantics be added incrementally, one term at a time, rather than committed to up front. Where every term is intended to reach RDF, a schema <em class="rfc2119">MAY</em> declare <code>@vocab</code> in its <code>@context</code>, mapping each otherwise-unmapped term into a default namespace, or carry the mapping in <a href="#synonyms"><code>x-oold-context</code></a>, whose entries are promoted into the <code>@context</code> before a JSON-LD processor runs. <span class="rule" id="OOLD-SCH-2d05" tabindex="0" role="link" title="OOLD-SCH-2d05 - An unmapped term is not a conformance failure, though a tool may report it under an opt-in strict mode.">An implementation <em class="rfc2119">MUST NOT</em> treat an unmapped term as a conformance failure, though it <em class="rfc2119">MAY</em> report one as guidance, or reject it under a strict mode the user opts into.</span></p>
<p>Leaving a term unmapped is a deferral, not a destination. <span class="rule" id="OOLD-SCH-21d7" tabindex="0" role="link" title="OOLD-SCH-21d7 - A schema should offer at least one complete mapping, so its instances can round-trip through RDF without loss.">A schema <em class="rfc2119">SHOULD</em> offer at least one <strong>complete</strong> mapping - a <a href="#synonyms">target profile</a> under which every declared property carries a term - because a property with no term produces no triple, so an instance can only round-trip through RDF without loss under a mapping that covers all of them.</span> A schema <em class="rfc2119">MAY</em> carry further, deliberately partial profiles alongside it.</p>
<div class="note" title="Inheritance"><p>A class <em>B</em> extends a class <em>A</em> by referencing it in both <code>allOf</code> (so JSON Schema validators apply <em>A</em>'s rules when validating <em>B</em> instances) and <code>@context</code> (so JSON-LD processors resolve <em>A</em>'s term mappings). <em>B</em> instances are therefore valid <em>A</em> instances and carry all of <em>A</em>'s properties alongside <em>B</em>'s own additions. Building types from <em>multiple</em> independent schemas is covered in <a href="#composition"></a>.</p></div>
<aside class="example" title="Inheritance and instantiation"><div class="mermaid">%%{init: {'theme': 'neutral' } }%%
classDiagram
class OOLD_Class_A {
JSON Schema + JSON-LD
@context: ...
properties: a...
}
class OOLD_Class_B {
JSON Schema + JSON-LD
@context: ./A
allOf: ./A
properties: b...
}
class OOLD_Instance_B {
JSON
@context: ./B
$schema: ./B
a: ...
b: ...
}
OOLD_Class_A <-- OOLD_Class_B: extends
OOLD_Class_B <-- OOLD_Instance_B: type
</div></aside></section>
<section id="composition"><h2>Composition</h2><p>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 <code>@context</code> is assembled automatically, so that no post-processing step is needed.</p>
<p><span class="rule" id="OOLD-CMP-b926" tabindex="0" role="link" title="OOLD-CMP-b926 - A schema must be usable as a JSON-LD context with no further processing, so every $ref to an OO-LD schema is reflected in the @context.">It <em class="rfc2119">MUST NOT</em> be required to further process an OO-LD schema document in order to interpret it as a JSON-LD context, so every <code>$ref</code> whose target is itself an [=OO-LD schema=] <em class="rfc2119">MUST</em> be reflected in the JSON-LD context.</span> A <code>$ref</code> to a plain JSON Schema carries no terms and therefore reflects nothing. <span class="rule" id="OOLD-CMP-5266" tabindex="0" role="link" title="OOLD-CMP-5266 - An embedded object property should be reflected as that property's scoped JSON-LD context.">An embedded object reached under an <strong>object-valued</strong> property - one whose value is an object, directly (<code>type: object</code>) or as the <code>items</code> of an array (<code>type: array</code>), whether inlined or brought in by <code>$ref</code> - <em class="rfc2119">SHOULD</em> be reflected as that property's <strong>scoped</strong> JSON-LD context, so its terms resolve only under that property and cannot conflict with a same-named term elsewhere.</span> That scoped context <em class="rfc2119">MAY</em> reference the embedded schema remotely (by URL) or carry its terms inline. Where the embed graph is <strong>cyclic</strong> - a value type whose scoped context transitively references itself through remote schema files - JSON-LD processors cannot resolve the recursive remote contexts (see <a href="#round-trip"></a>); breaking the cycle requires <strong>migrating the remote reference to a local (inline) context</strong> - inlining the term definitions so there is no remote hop to recurse - which <em class="rfc2119">MAY</em> be flattened onto the root context as a shared vocabulary. Moving the remote reference to the root does <em>not</em> break the cycle; only replacing it with local definitions does. A <code>$ref</code> 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 <a href="#range-of-properties"><code>x-oold-range</code></a>, not a <code>$ref</code>, and so contributes no scoped context.) In case of multiple <code>$ref</code> within <code>allOf</code> the corresponding remote contexts are merged into an array-valued <code>@context</code> (see <a href="#merging-remote-contexts"></a>). For <code>oneOf</code> / <code>anyOf</code> this requires care to avoid conflicts. At any time the importing OO-LD schema <em class="rfc2119">MAY</em> define its own or override the imported JSON-LD context.</p>
<aside class="example" title="Reflecting each `$ref` into the `@context`"><div class="ex-tabs">
<div class="ex-tablist" role="tablist"><button class="ex-tab" role="tab" aria-selected="true" data-panel="ex2j">JSON</button><button class="ex-tab" role="tab" aria-selected="false" data-panel="ex2y">View as YAML</button></div>
<div class="ex-panel" id="ex2j" role="tabpanel">
<pre><code class="language-json">{
"@context": [
"B.schema.json",
"P0.schema.json",
{ "p1": { "@context": "P1.schema.json" } },
{ "p2": { "@context": ["P2a.schema.json", "P2b.schema.json"] } },
{ "p3": { "@context": { "keyword_in_P3a": "ex:Property1", "keyword_in_P3b": "ex:Property2" } } }
],
"$id": "A.schema.json",
"allOf": [ { "$ref": "B.schema.json" } ],
"properties": {
"p0": { "type": "string", "$ref": "P0.schema.json" },
"p1": { "type": "object", "$ref": "P1.schema.json" },
"p2": { "type": "object", "allOf": [ { "$ref": "P2a.schema.json" }, { "$ref": "P2b.schema.json" } ] },
"p3": { "oneOf": [ { "$ref": "P3a.schema.json" }, { "$ref": "P3b.schema.json" } ] }
}
}
</code></pre>
</div>
<div class="ex-panel" id="ex2y" role="tabpanel" hidden>
<pre><code class="language-yaml">'@context':
- B.schema.json
- P0.schema.json
- p1:
'@context': P1.schema.json
- p2:
'@context':
- P2a.schema.json
- P2b.schema.json
- p3:
'@context':
keyword_in_P3a: ex:Property1
keyword_in_P3b: ex:Property2
$id: A.schema.json
allOf:
- $ref: B.schema.json
properties:
p0:
type: string
$ref: P0.schema.json
p1:
type: object
$ref: P1.schema.json
p2:
type: object
allOf:
- $ref: P2a.schema.json
- $ref: P2b.schema.json
p3:
oneOf:
- $ref: P3a.schema.json
- $ref: P3b.schema.json
</code></pre>
</div>
</div></aside>
<aside class="example" title="A nested context and the RDF it produces"><p>A <code>Pet</code> schema:</p>
<div class="ex-tabs">
<div class="ex-tablist" role="tablist"><button class="ex-tab" role="tab" aria-selected="true" data-panel="ex3j">JSON</button><button class="ex-tab" role="tab" aria-selected="false" data-panel="ex3y">View as YAML</button></div>
<div class="ex-panel" id="ex3j" role="tabpanel">
<pre><code class="language-json">{
"$schema": "https://oo-ld.org/latest/meta/oold-meta-schema.json",
"$id": "Pet.schema.json",
"@context": {
"ex": "https://example.org/",
"name": "ex:petName"
},
"title": "Pet",
"type": "object",
"properties": {
"name": { "type": "string", "description": "Name of the pet" }
}
}
</code></pre>
</div>
<div class="ex-panel" id="ex3y" role="tabpanel" hidden>
<pre><code class="language-yaml">$schema: https://oo-ld.org/latest/meta/oold-meta-schema.json
$id: Pet.schema.json
'@context':
ex: https://example.org/
name: ex:petName
title: Pet
type: object
properties:
name:
type: string
description: Name of the pet
</code></pre>
</div>
</div>
<p>A <code>PersonWithPet</code> schema that embeds <code>Pet</code> under a property-scoped context:</p>
<div class="ex-tabs">
<div class="ex-tablist" role="tablist"><button class="ex-tab" role="tab" aria-selected="true" data-panel="ex4j">JSON</button><button class="ex-tab" role="tab" aria-selected="false" data-panel="ex4y">View as YAML</button></div>
<div class="ex-panel" id="ex4j" role="tabpanel">
<pre><code class="language-json">{
"$schema": "https://oo-ld.org/latest/meta/oold-meta-schema.json",
"$id": "PersonWithPet.schema.json",
"@context": {
"ex": "https://example.org/",
"schema": "http://schema.org/",
"name": "schema:name",
"pets": { "@id": "ex:hasPet", "@container": "@set", "@context": "Pet.schema.json" }
},
"title": "Person with pet",
"type": "object",
"properties": {
"name": { "type": "string", "description": "Name of the person" },
"pets": {
"type": "array",
"description": "Pets owned by the person (embedded via a property-scoped context)",
"items": { "$ref": "Pet.schema.json" }
}
}
}
</code></pre>
</div>
<div class="ex-panel" id="ex4y" role="tabpanel" hidden>
<pre><code class="language-yaml">$schema: https://oo-ld.org/latest/meta/oold-meta-schema.json
$id: PersonWithPet.schema.json
'@context':
ex: https://example.org/
schema: http://schema.org/
name: schema:name
pets:
'@id': ex:hasPet
'@container': '@set'
'@context': Pet.schema.json
title: Person with pet
type: object
properties:
name:
type: string
description: Name of the person
pets:
type: array
description: Pets owned by the person (embedded via a property-scoped context)
items:
$ref: Pet.schema.json
</code></pre>
</div>
</div>
<p>An instance:</p>
<div class="ex-tabs">
<div class="ex-tablist" role="tablist"><button class="ex-tab" role="tab" aria-selected="true" data-panel="ex5j">JSON</button><button class="ex-tab" role="tab" aria-selected="false" data-panel="ex5y">View as YAML</button></div>
<div class="ex-panel" id="ex5j" role="tabpanel">
<pre><code class="language-json">{
"@context": "PersonWithPet.schema.json",
"$schema": "PersonWithPet.schema.json",
"name": "Max",
"pets": [ { "name": "Bruno" } ]
}
</code></pre>
</div>
<div class="ex-panel" id="ex5y" role="tabpanel" hidden>
<pre><code class="language-yaml">'@context': PersonWithPet.schema.json
$schema: PersonWithPet.schema.json
name: Max
pets:
- name: Bruno
</code></pre>
</div>
</div>
<p>Referencing <code>PersonWithPet.schema.json</code> as the context is equivalent to inlining the reflected contexts:</p>
<pre><code class="language-json">{
"@context": {
"name": "schema:name",
"pets": { "@id": "ex:hasPet", "@context": { "name": "ex:petName" } }
},
"$schema": "PersonWithPet.schema.json",
"name": "Max",
"pets": [ { "name": "Bruno" } ]
}
</code></pre>
<p>which expands (via the reflected contexts) to the RDF:</p>
<pre><code class="language-ttl">_:b0 <ex:hasPet> _:b1 .
_:b0 <schema:name> "Max" .
_:b1 <ex:petName> "Bruno" .
</code></pre></aside><section id="merging-remote-contexts"><h3>Merging remote contexts</h3><p><strong>Multiple <code>$ref</code> (e.g. in <code>allOf</code>)</strong> each correspond to a remote context. <span class="rule" id="OOLD-CMP-e4a3" tabindex="0" role="link" title="OOLD-CMP-e4a3 - A schema with multiple $refs must list their remote contexts as an array, in allOf order.">By the reflection rule above, the schema's own <code>@context</code> <em class="rfc2119">MUST</em> list those remote contexts as an array, in the same order as the <code>allOf</code> members, so the schema stays usable as a context without further processing.</span> 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 <em class="rfc2119">MAY</em> append its own context object as the last array entry to override an inherited term. The single-context <code>@import</code> keyword is an alternative only when exactly one remote context is wrapped and locally modified (it cannot contain a nested <code>@import</code>), so the array form is used for the multi-<code>$ref</code> case.</p>
<p><strong><code>oneOf</code> / <code>anyOf</code>.</strong> <span class="rule" id="OOLD-CMP-1d7e" tabindex="0" role="link" title="OOLD-CMP-1d7e - Reflected oneOf/anyOf branch contexts must not map the same keyword to different IRIs at the root.">The remote contexts of <code>oneOf</code> / <code>anyOf</code> branches <em class="rfc2119">MAY</em> also be reflected into the <code>@context</code>, but they <em class="rfc2119">MUST NOT</em> conflict at the root - they <em class="rfc2119">MUST NOT</em> map the same keyword to different IRIs there.</span> 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.</p>
<p><span class="rule" id="OOLD-CMP-6d7b" tabindex="0" role="link" title="OOLD-CMP-6d7b - Branch-specific mappings for the same keyword must be scoped rather than placed at the root.">Where branches genuinely need different mappings for the same keyword, those mappings <em class="rfc2119">MUST</em> be scoped with JSON-LD scoped contexts so each applies only where its branch applies, rather than placed at the root, since colliding root mappings are resolved by context order instead of by the branch the data conforms to:</span></p>
<ul>
<li><p><strong>Type-scoped contexts</strong> when the branches are distinguished by <code>@type</code>. The
scoped <code>@context</code> is attached to the term used as the type value and is activated only for nodes carrying that <code>@type</code>:</p>
<pre><code class="language-json">{
"@context": {
"Sensor": { "@id": "ex:Sensor", "@context": { "reading": "ex:temperature" } },
"Gauge": { "@id": "ex:Gauge", "@context": { "reading": "ex:pressure" } }
}
}
</code></pre>
</li>
</ul>
<p>Here <code>{ "@type": "Sensor", "reading": 21 }</code> maps <code>reading</code> to <code>ex:temperature</code>, while <code>{ "@type": "Gauge", "reading": 3 }</code> maps the same keyword to <code>ex:pressure</code>.</p>
<ul>
<li><strong>Property-scoped contexts</strong> when the conflicting keyword appears under
different parent properties. The mapping is attached to the parent property's term (its <code>@context</code>) and applies only within that property's value, so the same keyword can resolve differently under different parents.</li>
</ul>
<p><strong>Propagation (<code>@propagate</code>).</strong> A <code>$ref</code> inside a <code>type: object</code> 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"). <span class="rule" id="OOLD-CMP-a05a" tabindex="0" role="link" title="OOLD-CMP-a05a - A scoped context that must apply only to the immediate node sets @propagate false; contexts in one array share it.">Where a referenced context should apply only to the immediate node, the schema <em class="rfc2119">MUST</em> set <code>"@propagate": false</code> on that scoped context.</span></p>
<p><strong>Protected terms (<code>@protected</code>).</strong> A schema <em class="rfc2119">MAY</em> mark terms <code>@protected</code> to prevent later contexts from silently redefining them. When contexts are combined via <code>allOf</code>, redefining a protected term to a different IRI is a <code>protected term redefinition</code> error under [[JSON-LD11]] unless the new definition is identical; property-scoped contexts are exempt and may override protected terms within their subtree. Relying on <code>@protected</code> therefore constrains which schemas a schema can be combined with.</p>
<p><strong>Independent references and base URIs.</strong> A JSON Schema <code>$ref</code> and a JSON-LD <code>@context</code> entry are independent references: they <em class="rfc2119">MAY</em> 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 <code>$ref</code> together with a separate remote <code>@context</code> that supplies the semantics. <span class="rule" id="OOLD-CMP-53bf" tabindex="0" role="link" title="OOLD-CMP-53bf - A schema's JSON Schema and JSON-LD base URIs should be aligned so a relative reference resolves the same under both.">Relative references resolve against the schema's <code>$id</code> (the JSON Schema base URI) and, on the JSON-LD side, against <code>@base</code> / the retrieval URL; these base URIs <em class="rfc2119">SHOULD</em> be aligned so a relative reference resolves to the same absolute URL under both.</span> <span class="rule" id="OOLD-CMP-dd2b" tabindex="0" role="link" title="OOLD-CMP-dd2b - A schema $id must not contain a non-empty fragment."><code>$id</code> <em class="rfc2119">MUST NOT</em> contain a non-empty fragment ([[JSONSCHEMA]] §8.2.1).</span></p>
<p>Where the two references resolve to <strong>different</strong> documents, the targets are governed by their own specifications and carry no OO-LD guarantees: the <code>$ref</code> target is a JSON Schema, the context target is a JSON-LD context, and neither is an [=OO-LD schema=]. Where they resolve to the <strong>same</strong> document, that coincidence is itself the signal. Where a <code>$ref</code> and an <code>@context</code> reference resolve to the same document, a consumer <em class="rfc2119">MAY</em> treat that document as an [=OO-LD schema=] and rely on both halves being present, without any further declaration on the referring side. The guarantees come from the upstream specifications rather than from OO-LD: the context reference cannot have resolved unless the document carries <code>@context</code> ([[JSONLD11-API]] aborts with <code>invalid remote context</code> otherwise), and a <code>$ref</code> target is at minimum a valid JSON Schema, the empty schema being valid. No additional keyword marks an embedded target as an OO-LD document; adding one would duplicate what both upstream references already carry.</p></section><section id="merge-and-override-model"><h3>Merge and override model</h3><p>JSON Schema's validation algebra is purely conjunctive: <code>allOf</code> requires an instance to satisfy every subschema, and the specification defines no merge of the subschemas' keyword <em>values</em> ([[JSONSCHEMA]] §10.2.1.1). Several consumers nonetheless require a single <em>[=resolved schema=]</em> view rather than a conjunction: a UI generator needs exactly one <code>title</code> per field, a code generator flattens an inheritance chain into one class, and an OO-LD preprocessor must produce one <code>@context</code>.</p>
<p>When such a merge is required, OO-LD resolves the <code>allOf</code> chain by applying <strong>JSON Merge Patch ([[RFC7396]]) semantics</strong>: keyed by object member, most-recently-defined (most-derived) wins, and a <code>null</code> value removes a key. For the <code>@context</code> this coincides with JSON-LD's own override rule. <span class="rule" id="OOLD-CMP-f3c7" tabindex="0" role="link" title="OOLD-CMP-f3c7 - Composition is narrow-only: a derived schema may restrict a constraint but must not relax it.">For assertion-bearing keywords the resolved view additionally honors <strong>narrow-only</strong> composition: a derived schema <em class="rfc2119">MAY</em> restrict a constraint but <em class="rfc2119">MUST NOT</em> relax it, matching how code generators let a subclass tighten - never loosen - a superclass property's validation.</span></p>
<p>This single model governs every place OO-LD collapses composition into a resolved value: single-valued annotations such as <code>title</code> and <code>x-oold-multilang-title</code> 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.</p></section><section id="closing-composed-objects"><h3>Closing composed objects</h3><p>Because <code>allOf</code> composition is conjunctive, <code>additionalProperties: false</code> on one subschema rejects the members that the other composed subschemas contribute, so it cannot close a composed object. <span class="rule" id="OOLD-CMP-1257" tabindex="0" role="link" title="OOLD-CMP-1257 - A composed object is closed with unevaluatedProperties, not additionalProperties.">A schema that closes a composed object <em class="rfc2119">MUST</em> do so with <code>unevaluatedProperties: false</code> ([[JSONSCHEMA]] §11.3), which is evaluated after the <code>allOf</code> branches and therefore accounts for their properties, rather than with <code>additionalProperties: false</code>, which does not see them and so rejects instances the composition permits.</span> Note that <a href="#schema-instances"></a> requires such a schema to still permit the instance-level <code>$schema</code> and <code>@context</code> members.</p>
<aside class="example" title="Closing an inherited object with `unevaluatedProperties`"><pre><code class="language-json">{
"$id": "Employee.schema.json",
"@context": {},
"allOf": [ { "$ref": "Person.schema.json" } ],
"properties": {
"employeeId": { "type": "string" },
"$schema": { "type": "string" },
"@context": {}
},
"unevaluatedProperties": false
}
</code></pre>
<p>An instance may carry the <code>Person</code> properties, <code>employeeId</code>, and <code>$schema</code> / <code>@context</code>; any other member is rejected.</p></aside></section></section>
<section id="schema-instances"><h2>Schema Instances</h2><p>An [=OO-LD instance=] is a JSON document that conforms to an [=OO-LD schema=]. It references that schema in two ways:</p>
<ul>
<li><code>@context</code> - the schema URL, loaded as a JSON-LD remote context. This is what makes the instance a JSON-LD document.</li>
<li><code>$schema</code> - the schema URL, identifying the schema the instance is intended to validate against.</li>
</ul>
<aside class="example" title="An instance referencing its schema"><pre><code class="language-json">{
"@context": "https://example.org/my-package/1.0.0/Person.schema.json",
"$schema": "https://example.org/my-package/1.0.0/Person.schema.json"
}
</code></pre></aside>
<p><span class="rule" id="OOLD-INS-cb1a" tabindex="0" role="link" title="OOLD-INS-cb1a - An instance's @context and $schema should point at the same schema URL.">The two <em class="rfc2119">SHOULD</em> point at the same schema URL, so that the context an instance is read with and the schema it is validated against are the same document.</span> <span class="rule" id="OOLD-INS-9416" tabindex="0" role="link" title="OOLD-INS-9416 - Instances should reference a versioned schema URL.">Instances <em class="rfc2119">SHOULD</em> use a versioned schema URL so that it is unambiguous which schema version they conform to.</span></p><section id="referencing-schema"><h3>Referencing the schema with <code>$schema</code></h3><p>In standard JSON Schema, <code>$schema</code> 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 <code>$schema</code> 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. <a href="https://github.com/thiagowfx/check-json-schema-meta">check-json-schema-meta</a>) honor it. Where the instance is served over HTTP, the standards-conformant alternative is the <code>describedby</code> link relation ([[JSONSCHEMA]] §9.5.1.1), optionally with the <code>profile</code> media-type parameter ([[RFC6906]]):</p>
<aside class="example" title="Linking an instance to its schema over HTTP"><pre><code class="language-http">Link: <https://example.org/my-package/1.0.0/Person.schema.json>; rel="describedby"
</code></pre></aside>
<p><span class="rule" id="OOLD-INS-78df" tabindex="0" role="link" title="OOLD-INS-78df - A tool resolves an instance's schema from $schema, then the @context URL, then an inline type, in that order.">An OO-LD-aware tool <em class="rfc2119">MUST</em> determine an instance's schema in the following order, taking the first that applies:</span></p>
<ol>
<li>the <code>$schema</code> value, if present;</li>
<li>otherwise, the URL given under <code>@context</code>, if the referenced document declares itself to be an OO-LD schema;</li>
<li>otherwise, an inline <code>@type</code> (see below) - but only when at least one of the type IRIs resolves to an OO-LD schema.</li>
</ol>
<p><span class="rule" id="OOLD-INS-f010" tabindex="0" role="link" title="OOLD-INS-f010 - A consuming side must not be assumed to hold an rdf:type-to-schema registry; exports are self-sufficient.">An implementation <em class="rfc2119">MAY</em> additionally maintain a registry mapping <code>rdf:type</code> IRIs to OO-LD schemas to resolve case 3, but such a registry <em class="rfc2119">MUST NOT</em> be assumed to exist on the consuming side - so exports are self-sufficient (see below).</span></p>
<p><span class="rule" id="OOLD-INS-ba9e" tabindex="0" role="link" title="OOLD-INS-ba9e - A schema closing its objects must still permit the $schema and @context members.">Because an instance carries <code>$schema</code> and <code>@context</code> as ordinary members, an OO-LD schema that closes its objects with <code>additionalProperties: false</code> or <code>unevaluatedProperties: false</code> <em class="rfc2119">MUST</em> permit these two members, or conforming instances would fail validation.</span></p>
<p><code>@context</code> already provides a JSON-LD-native link to the schema (resolution case 2 above), so <code>$schema</code> is kept primarily for compatibility with the widespread editor and CI convention, not as a second authoritative mechanism. <span class="rule" id="OOLD-INS-27aa" tabindex="0" role="link" title="OOLD-INS-27aa - A consumer should not blindly trust the schema an instance declares for itself.">JSON Schema deliberately does not standardize <code>$schema</code> on instances, partly over a self-validation concern: a consumer <em class="rfc2119">SHOULD NOT</em> 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.</span></p></section><section id="identity"><h3>Identity (<code>@id</code>)</h3><p>An instance that represents an identifiable entity is identified by an <code>@id</code> - the IRI of that entity. This is the JSON-LD node identifier, distinct from <code>$schema</code> / <code>@context</code> (which identify the schema) and from the schema's own <code>$id</code> / <code>x-oold-uuid</code>. Without an <code>@id</code> the entity is an anonymous blank node and cannot be referenced (for example as the target of an <code>x-oold-range</code> or <code>@reverse</code> relation).</p>
<p><span class="rule" id="OOLD-INS-2b3f" tabindex="0" role="link" title="OOLD-INS-2b3f - Schemas should expose @id through an aliased id property.">To keep instance keys variable-name-friendly, schemas <em class="rfc2119">SHOULD</em> expose <code>@id</code> through an aliased <code>id</code> property (as with <code>type</code> -> <code>@type</code>):</span></p>
<aside class="example" title="Aliasing `@id` to `id`"><pre><code class="language-json">{
"@context": {
"id": "@id"
},
"properties": {
"id": {
"type": "string"
}
}
}
</code></pre></aside>
<p><span class="rule" id="OOLD-INS-1d33" tabindex="0" role="link" title="OOLD-INS-1d33 - An exported identifiable entity must carry an IRI.">An implementation <em class="rfc2119">MAY</em> use a non-IRI identifier internally, but when it <strong>exports</strong> an identifiable entity (to JSON-LD / RDF) it <em class="rfc2119">MUST</em> assign an <code>@id</code> (or the aliased <code>id</code>).</span> <span class="rule" id="OOLD-INS-cd80" tabindex="0" role="link" title="OOLD-INS-cd80 - An instance @id should be resolvable, and is recommended to be minted from an autogenerated UUID.">The <code>@id</code> <em class="rfc2119">SHOULD</em> be resolvable, and it is <em class="rfc2119">RECOMMENDED</em> to mint it from an autogenerated UUID - mirroring the schema's <code>x-oold-uuid</code> - e.g. <code>https://example.org/a1b2c3d4-1234-...</code>.</span></p>
<p>Embedded value objects that have no independent identity (for example an <code>Address</code> embedded in an <code>Organization</code>) <em class="rfc2119">MAY</em> omit <code>@id</code> and remain blank nodes.</p></section><section id="semantic-type"><h3>Carrying the semantic type</h3><p>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.</p>
<p>A schema declares the <code>rdf:type</code>(s) of its instances with the <code>x-oold-instance-rdf-type</code> keyword (always a list of IRIs, e.g. <code>["schema:Person"]</code>):</p>
<aside class="example" title="Declaring the instance rdf:type on the schema"><pre><code class="language-json">{ "x-oold-instance-rdf-type": ["schema:Person"] }
</code></pre></aside>
<p>These types live in the schema, not in the instance data, so a JSON-LD-only processor - which sees only the instance and its <code>@context</code> - cannot derive them. <span class="rule" id="OOLD-INS-75c6" tabindex="0" role="link" title="OOLD-INS-75c6 - Tooling exporting an instance must materialize the schema-declared rdf:type(s) as @type.">Therefore, when OO-LD tooling <strong>exports</strong> an instance (to JSON-LD / RDF), it <em class="rfc2119">MUST</em> materialize the declared <code>rdf:type</code>(s) as an <code>@type</code> on the instance, so that the type reaches RDF without access to the schema or to a type registry.</span></p>
<p>A materialized <code>@type</code> is an ordinary JSON-LD <code>@type</code>: its IRIs resolve through the <code>@context</code> like any term, so a class name declared as a context term (<code>"QuantityValue": "qudt:QuantityValue"</code>) is aliased on export, carries <a href="#synonyms">term synonyms</a>, and drives <a href="#framing">frame</a> construction. <code>x-oold-instance-rdf-type</code> and an inline <code>type</code> are thus two ways to reach the same <code>@type</code>: the former lets an application keep the type in the schema alone (no <code>@type</code> 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 <code>@type</code> semantics: a value written as a <strong>term</strong> (a plain string such as <code>"QuantityValue"</code>) resolves through the <code>@context</code> and so is aliased per profile if that term carries synonyms, exactly as an inline <code>type</code> term is, whereas a value written as a full IRI or CURIE (<code>"qudt:QuantityValue"</code>, <code>"http://qudt.org/..."</code>) is emitted verbatim and not aliased. This holds for a materialized <code>x-oold-instance-rdf-type</code> and an inline <code>type</code> alike - the author selects term form for a profile-aliasable type, IRI form for a fixed one. Multiplicity and aliasing are orthogonal: <code>x-oold-instance-rdf-type</code> <em>co-types</em> - it lists every <code>rdf:type</code> an instance asserts at once (e.g. <code>qudt:QuantityValue</code> and <code>schema:QuantitativeValue</code> together) - while <code>x-oold-context</code> on a type term supplies the <em>synonyms</em> of a single type, one chosen per profile; a co-typed type may itself carry synonyms, so the two compose.</p>
<p>Alternatively, an instance <em class="rfc2119">MAY</em> carry the type inline as a <code>type</code> property (self-describing data). The schema maps the <code>type</code> term to the JSON-LD keyword <code>@type</code> and gives it a <code>default</code>. Because <code>type</code> is modeled as a strict array here, its term also declares <code>@container: "@set"</code> - like any strict-array property (see <a href="#round-trip"></a>) - so a single <code>@type</code> value returns from RDF as a one-element array and re-validates, rather than compacting to a scalar. No <code>@type: @id</code> coercion is needed because JSON-LD already interprets <code>@type</code> values as IRIs; the property named <code>type</code> is distinct from the JSON Schema <code>type</code> keyword, and its value <em class="rfc2119">MAY</em> be a single IRI or a list of IRIs:</p>
<aside class="example" title="Carrying the type inline"><pre><code class="language-json">{
"@context": {
"schema": "http://schema.org/",
"type": { "@id": "@type", "@container": "@set" }
},
"type": "object",
"properties": {
"type": {
"type": "array",
"items": { "type": "string" },
"default": ["schema:Person"]
}
}
}
</code></pre></aside>
<div class="note" title="Why `default` and not `const`"><p><code>const</code> would also fix the type, but it would prevent a subclass from overriding or extending it (for example a subclass adding <code>schema:Researcher</code>). With <code>default</code> a subclass can redefine the property.</p></div>
<p><span class="rule" id="OOLD-INS-4b5c" tabindex="0" role="link" title="OOLD-INS-4b5c - An inline type must be consistent with the schema's x-oold-instance-rdf-type.">If an inline <code>type</code> is present it <em class="rfc2119">MUST</em> be consistent with the schema's <code>x-oold-instance-rdf-type</code>.</span> Note that <code>@type</code> alone lets a consumer locate the schema (case 3 above) only when one of the type IRIs resolves to an OO-LD schema.</p>
<p>Under composition, <code>x-oold-instance-rdf-type</code> follows the same most-derived-wins rule as the rest of the schema (see <a href="#composition"></a>). <span class="rule" id="OOLD-INS-559f" tabindex="0" role="link" title="OOLD-INS-559f - The nearest x-oold-instance-rdf-type declaration in an allOf chain replaces a base class's value rather than appending to it.">The nearest declaration in the <code>allOf</code> chain is authoritative and <em class="rfc2119">MUST</em> replace a base class's value rather than append to it.</span> Superclass types are recoverable by ontology inference (<code>rdfs:subClassOf</code>) and so need not be materialized; a schema that wants a supertype carried in the data lists it explicitly (e.g. <code>["schema:Researcher", "schema:Person"]</code>).</p></section><section id="value-forms"><h3>Property value forms</h3><p>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:</p>
<ul>
<li>a <strong>literal</strong> - a bare scalar (string, number, boolean), e.g. <code>"Mainstreet 1"</code>;</li>
<li>a <strong>reference</strong> - an object carrying an <code>@id</code> (or its <code>id</code> alias), e.g. <code>{ "id": "https://example.org/PlaceA" }</code>, denoting an independent entity by IRI;</li>
<li>an <strong>embedded object</strong> - a nested object with its own properties and no independent identity, e.g. <code>{ "type": "PostalAddress", "streetAddress": "..." }</code>, which becomes a blank node.</li>
</ul>
<aside class="example" title="One `address` property, three value forms"><p>Literal:</p>
<pre><code class="language-json">{ "address": "Mainstreet 1, 10115 Example City" }
</code></pre>
<p>Reference (by <code>id</code>/<code>@id</code>):</p>
<pre><code class="language-json">{ "address": { "id": "https://example.org/address/A1" } }
</code></pre>
<p>Embedded object:</p>
<pre><code class="language-json">{ "address": { "type": "PostalAddress", "streetAddress": "Mainstreet 1", "postalCode": "10115" } }
</code></pre></aside>
<p>A single <code>@context</code> term cannot interpret a bare string as <strong>both</strong> a literal and an IRI: <code>@type: "@id"</code> 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. <span class="rule" id="OOLD-INS-2e5d" tabindex="0" role="link" title="OOLD-INS-2e5d - A property whose range includes free text must not use @type @id.">A property whose range includes free text therefore <em class="rfc2119">MUST NOT</em> use <code>@type: "@id"</code>.</span> <span class="rule" id="OOLD-INS-770a" tabindex="0" role="link" title="OOLD-INS-770a - A reference written as a bare IRI string requires @type @id on its term.">A property whose range is references only <em class="rfc2119">MAY</em> be written as a bare IRI string, and where it is, its term <em class="rfc2119">MUST</em> carry <code>@type: "@id"</code>.</span></p>
<p><span class="rule" id="OOLD-INS-da1a" tabindex="0" role="link" title="OOLD-INS-da1a - A model ecosystem should adopt one of the two ambiguous-range patterns consistently.">For a property whose range mixes free text with references and/or embedded objects (for example <code>Text | PostalAddress | Place</code>), two patterns keep the instance round-trippable (see <a href="#round-trip"></a>); a model ecosystem <em class="rfc2119">SHOULD</em> adopt one of them consistently:</span></p>
<ol>
<li><strong>Value-form</strong> - a single plain term (no <code>@type: "@id"</code>); the value shape alone disambiguates: a bare scalar is a literal, <code>{ "id": ... }</code> is a reference, a typed object is embedded. <span class="rule" id="OOLD-INS-1df7" tabindex="0" role="link" title="OOLD-INS-1df7 - Under the value-form pattern a reference is written as an object and its term must not carry @type.">Under the value-form pattern a reference <em class="rfc2119">MUST</em> be written as an object, and the term <em class="rfc2119">MUST NOT</em> carry <code>@type</code>.</span></li>
<li><strong>Separate keys</strong> - a canonical term <code>p</code> with <code>@type: "@id"</code> (a bare IRI string reference, plus embedded objects via a scoped <code>@context</code>) and a companion <code>p_text</code> that is a plain term for the literal.</li>
</ol>
<aside class="example" title="The two patterns for an ambiguous `address` term"><p>Value-form - one plain term; the value shape disambiguates:</p>
<pre><code class="language-json">{
"address": {
"@id": "schema:address",
"@context": { "PostalAddress": { "@id": "schema:PostalAddress" } }
}
}
</code></pre>
<p>Separate keys - a canonical <code>@type: "@id"</code> term for references and embedded objects, plus a plain <code>address_text</code> companion for the literal:</p>
<pre><code class="language-json">{
"address": {
"@id": "schema:address",
"@type": "@id",
"@context": { "PostalAddress": { "@id": "schema:PostalAddress" } }
},
"address_text": { "@id": "schema:address" }
}
</code></pre></aside>
<p><span class="rule" id="OOLD-RT-d9bd" tabindex="0" role="link" title="OOLD-RT-d9bd - A term must not coerce a literal to a datatype JSON-LD produces by default from a native JSON value (xsd:string, xsd:boolean, xsd:integer, xsd:double).">A term <em class="rfc2119">MUST NOT</em> declare <code>@type</code> with a datatype that JSON-LD produces by <em>default</em> from a native JSON value: <code>xsd:string</code> (from a string), <code>xsd:boolean</code> (from a boolean), <code>xsd:integer</code> (from an integer number), and <code>xsd:double</code> (from a fractional number).</span> These are exactly the datatypes reconstruction converts back to native JSON values without an <code>@type</code> ([[JSONLD11-API]], RDF to Object Conversion; see <a href="#round-trip"></a>): 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 <code>xsd:integer</code> or <code>xsd:double</code> 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 <code>@type</code>), and the projection to RDF still yields the correct datatype from the native JSON type ([[JSONLD11-API]], Data Round Tripping). The behaviour assumes reconstruction <em>with</em> native types (<code>useNativeTypes</code>), 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 <code>{ "@value": ..., "@type": ... }</code> objects), which defeats the structural model - so native-type reconstruction is assumed throughout.</p>
<p>Datatypes JSON-LD does <em>not</em> produce by default are the ones to declare with <code>@type</code>: the date/time family (<code>xsd:date</code>, <code>xsd:dateTime</code>, <code>xsd:time</code>, <code>xsd:duration</code>), <code>xsd:anyURI</code>, and every numeric refinement outside the two native ones (<code>xsd:float</code>, <code>xsd:decimal</code>, <code>xsd:long</code>, <code>xsd:int</code>, <code>xsd:unsignedByte</code>, ...). 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 <strong>JSON string</strong>. The consequence is worth stating plainly: a JSON-native number can only ever be <code>xsd:integer</code> or <code>xsd:double</code>; any narrower or more specific numeric datatype is reached by writing the value as a string under an <code>@type</code> coercion (a bare JSON number under, say, <code>@type: "xsd:float"</code> keeps the term but comes back as its canonical string form). <span class="rule" id="OOLD-RT-ad63" tabindex="0" role="link" title="OOLD-RT-ad63 - A numeric property should not be coerced to a narrower datatype unless the exact RDF datatype matters.">A numeric property <em class="rfc2119">SHOULD NOT</em> be coerced to a narrower datatype, and its values <em class="rfc2119">SHOULD</em> be left as native JSON numbers, unless the exact RDF datatype matters.</span></p>
<p>Whichever pattern is chosen, <code>x-oold-range</code> records the permitted target type(s) on the reference and embedded branches (see <a href="#range-of-properties"></a>).</p>
<div class="note" title="Cached members beside `@id` are a convention, not a signal"><p>A reference can carry sibling members beside <code>@id</code> (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 <em>cache</em> 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.</p></div></section><section id="round-trip"><h3>Projection to RDF and round-trip</h3><p>An OO-LD instance projects to RDF through its <code>@context</code> and can be reconstructed from RDF. Because RDF is an unordered set of triples over a flat graph, faithful reconstruction follows a fixed contract:</p>
<ul>
<li><strong>Literals and references</strong> reconstruct by JSON-LD compaction against the schema's <code>@context</code> alone.</li>
<li><strong>Embedded objects</strong> are flattened in RDF, and compaction does not re-nest a flat graph, so reconstructing the tree requires <a href="#framing">Framing</a> - the frame can be as small as <code>{ "<property>": {} }</code>. <span class="rule" id="OOLD-RT-a12c" tabindex="0" role="link" title="OOLD-RT-a12c - An embedded object must carry an explicit type where the scoped context is type-scoped or the reconstruction frame matches on @type.">The embedded object <em class="rfc2119">MUST</em> carry an explicit <code>type</code> where the property's scoped <code>@context</code> is <em>type-scoped</em> - keyed by the value's <code>@type</code> to distinguish several embedded types (or to stamp the node's <code>rdf:type</code>) - or where the frame used to reconstruct it matches on <code>@type</code>, as a frame derived from the schema's class type does (see <a href="#framing"></a>).</span> A <em>flat</em> scoped context reconstructed through a property-matching frame needs none. Where required, tooling materializes the type on export (see <a href="#semantic-type"></a>).</li>
<li><strong>Multi-valued properties</strong> are set-valued in RDF: order is not preserved, duplicates are removed, and a single value compacts to a scalar. <span class="rule" id="OOLD-RT-08f2" tabindex="0" role="link" title="OOLD-RT-08f2 - A strictly array-typed property must declare @container @set or @list.">Because the reconstruction <em class="rfc2119">MUST</em> re-validate, a property that is <em>strictly</em> an array (JSON Schema <code>type: "array"</code>) <em class="rfc2119">MUST</em> declare <code>@container: "@set"</code> (or <code>"@list"</code>): without it a single-element array returns as a scalar and violates the <code>array</code> type.</span> A property that also permits a scalar (an <code>anyOf</code>/<code>oneOf</code> of a literal and an array) <em class="rfc2119">MAY</em> 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. <span class="rule" id="OOLD-RT-2028" tabindex="0" role="link" title="OOLD-RT-2028 - A property should not declare @list unless the order of its values is significant.">A property <em class="rfc2119">SHOULD NOT</em> declare <code>@list</code> unless the order of its values is significant, since ordering costs merge and query ergonomics.</span></li>
<li><strong>Language-tagged text</strong> uses <code>@language</code> (or <code>@container: "@language"</code>) so multilingual instance values round-trip - distinct from <code>x-oold-multilang-title</code> / <code>x-oold-multilang-description</code>, which localize the schema's own annotations, not instance data.</li>
</ul>
<div class="note" title="Typed literals round-trip by value"><p>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 <code>40.75</code> returns as the number <code>40.75</code>, not its <code>"4.075E1"</code> lexical form), which a reconstruction tool obtains by reading RDF literals with native types (<code>jsonld.fromRDF({ useNativeTypes: true })</code>). Other datatypes - dates, durations, and any non-native <code>xsd:</code> type - return as their canonical lexical <em>string</em> (e.g. a normalized <code>xsd:date</code>). Round-trip equality for typed literals is value equality after datatype canonicalization.</p></div>
<p>An embedded object is mapped by a <em>scoped</em> <code>@context</code> on its property (referencing the embedded type's own context). <span class="rule" id="OOLD-RT-d376" tabindex="0" role="link" title="OOLD-RT-d376 - The embed graph formed by scoped @contexts should be acyclic.">These scoped contexts form an <strong>embed graph</strong> between schemas, and that graph <em class="rfc2119">SHOULD</em> 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 <strong>reference</strong> - <code>@type: "@id"</code> plus <code>x-oold-range</code>, with no scoped <code>@context</code> - rather than an embed.</span> This is the linked-data analog of using a pointer instead of inlining a recursive data structure. A <strong>self-reference through the top-level <code>@context</code></strong> (a property that nests the same type but carries no scoped context, so the global context maps the nested keys - e.g. a <code>Process</code> with sub-<code>Process</code>es) is <em>not</em> part of this graph and round-trips normally, bounded by the instance's actual depth.</p>
<div class="note" title="Cyclic scoped contexts are not processable in practice"><p>A JSON-LD processor validates a term's scoped <code>@context</code> 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 <em>validate scoped context</em> flag, the remote-context set, and a <code>context overflow</code> 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 <a href="#range-of-properties">Range of properties</a>).</p></div></section></section>
<section id="identification-versioning"><h2>Identification and Versioning</h2><section id="identification"><h3>Identification</h3><p><span class="rule" id="OOLD-VER-3b96" tabindex="0" role="link" title="OOLD-VER-3b96 - A schema must have a $id serving as its global unique identifier.">OO-LD schemas <em class="rfc2119">MUST</em> have a <code>$id</code> ([[JSONSCHEMA]] §8.2.1) which works as a global and unique identifier of the schema.</span> The value of <code>$id</code> <em class="rfc2119">MAY</em> be an absolute URI (details below). <span class="rule" id="OOLD-VER-2e63" tabindex="0" role="link" title="OOLD-VER-2e63 - A schema should be resolvable via its $id.">The schema <em class="rfc2119">SHOULD</em> be resolvable via this URI.</span> <span class="rule" id="OOLD-VER-edb9" tabindex="0" role="link" title="OOLD-VER-edb9 - A schema should carry an x-oold-uuid annotation holding a UUID value.">The schema <em class="rfc2119">SHOULD</em> have an annotation <code>x-oold-uuid</code> with a UUID value.</span></p>
<p><span class="rule" id="OOLD-VER-46b9" tabindex="0" role="link" title="OOLD-VER-46b9 - A schema that carries an x-oold-uuid should also use it in its $id.">A schema carrying <code>x-oold-uuid</code> <em class="rfc2119">SHOULD</em> also use that UUID in its <code>$id</code>, so the identifier stays stable across a move between hosts or paths and the two identifiers cannot disagree about which schema they name.</span></p>
<p>This obligation applies to a schema <em>document</em>, not to every subschema: a fragment inside <code>properties</code>, <code>$defs</code> or <a href="#range-of-properties"><code>x-oold-range</code></a> legitimately has no <code>$id</code> of its own. The dialect meta-schema enforces exactly that distinction and so checks the requirement itself - see <a href="#meta-schema"></a>.</p>
<aside class="example" title="A schema `$id` and its `x-oold-uuid`"><pre><code class="language-json">{
"$id": "https://example.org/Foo.schema.json",
"@context": {},
"x-oold-uuid": "b5203131-7321-46bb-8a11-acb3d1015840",
"title": "Foo"
}
</code></pre>
<p>The same schema, with the UUID carried in the <code>$id</code> as well:</p>
<pre><code class="language-json">{
"$id": "https://example.org/b5203131-7321-46bb-8a11-acb3d1015840.schema.json",
"@context": {},
"x-oold-uuid": "b5203131-7321-46bb-8a11-acb3d1015840",
"title": "Foo"
}
</code></pre></aside></section><section id="ontology-mapping"><h3>Ontology correspondence (schema-level <code>x-oold-sssom</code>)</h3><p>A schema states which external ontology resources it corresponds to with a top-level <code>x-oold-sssom</code> block - the <a href="#synonyms">term-synonym</a> mapping row lifted to the schema level. Its subject is the schema itself, identified by its <code>$id</code>: in OO-LD the schema document at that URL <em>is</em> the class definition, so <code>$id</code> serves as the mapping's <code>subject_id</code>. It is an object keyed by the <strong>object IRI</strong> of a resolvable resource (an RDF/OWL class, a controlled-vocabulary term, another schema), each value carrying the SSSOM slots: <code>predicate_id</code> (default <code>skos:exactMatch</code>; <code>skos:closeMatch</code> for a looser correspondence), <code>mapping_set_id</code>, and the rest of the open bag. Because these are SKOS mapping predicates the correspondence is non-logical and reasoner-safe: <code>skos:exactMatch</code> asserts interchangeability, not the logical <code>owl:equivalentClass</code> (a schema that wants the reasoner-affecting claim uses <code>owl:equivalentClass</code> explicitly as the <code>predicate_id</code>). <span class="rule" id="OOLD-VER-9846" tabindex="0" role="link" title="OOLD-VER-9846 - x-oold-sssom annotates the schema, so an exporter must not stamp it onto instances."><code>x-oold-sssom</code> is an annotation <strong>about the schema itself</strong>, and an exporter <em class="rfc2119">MUST NOT</em> stamp it onto instances - distinct from <code>x-oold-instance-rdf-type</code>, the <code>rdf:type</code>s instances carry on export (see <a href="#semantic-type"></a>), which <em>are</em> stamped onto instance data.</span></p>
<aside class="example" title="Distinguishing document URL, ontology correspondence, and instance type"><pre><code class="language-json">{
"$id": "https://example.org/my-package/1.0.0/Person.schema.json",
"@context": {},
"x-oold-sssom": {
"http://schema.org/Person": { "predicate_id": "skos:exactMatch" }
},
"x-oold-instance-rdf-type": ["schema:Person"],
"title": "Person"
}
</code></pre></aside>
<p>Here the schema document is fetched from the <code>$id</code> URL, it is declared an exact match for <code>schema:Person</code>, and instances exported to RDF carry <code>@type: schema:Person</code>. Commonly a schema's exact match and the entries in <code>x-oold-instance-rdf-type</code> resolve to the same IRI, but they may differ - a schema may model a more specific subclass inline while emitting a broader <code>rdf:type</code>, 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 <code>predicate_id</code> semantics, <code>mapping_set_id</code> selection and SSSOM round-trip (see <a href="#synonyms"></a>).</p>
<p>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.</p></section><section id="versioning"><h3>Versioning</h3><p><span class="rule" id="OOLD-VER-3662" tabindex="0" role="link" title="OOLD-VER-3662 - A schema version should be stated with x-oold-version.">The schema version <em class="rfc2119">SHOULD</em> be indicated by <code>x-oold-version</code>; a prior version <em class="rfc2119">MAY</em> be indicated with <code>x-oold-prior-version</code>:</span></p>
<p><span class="rule" id="OOLD-VER-b2ee" tabindex="0" role="link" title="OOLD-VER-b2ee - Schema versions should follow semantic versioning.">Versions <em class="rfc2119">SHOULD</em> follow <a href="https://semver.org/">semantic versioning</a>, so that the version alone tells a consumer whether an instance that validated against an earlier version still validates: a patch or minor release is backward compatible, a major release is not.</span> <code>x-oold-backward-compatible-with</code> and <code>x-oold-incompatible-with</code> then record the exceptions rather than the rule.</p>
<aside class="example" title="Version annotations"><pre><code class="language-json">{
"$id": "https://example.org/b5203131-7321-46bb-8a11-acb3d1015840.schema.json",
"@context": {},
"x-oold-uuid": "b5203131-7321-46bb-8a11-acb3d1015840",
"title": "Foo",
"x-oold-version": "1.1.0",
"x-oold-prior-version": "1.0.0"
}
</code></pre></aside>
<p><span class="rule" id="OOLD-VER-534a" tabindex="0" role="link" title="OOLD-VER-534a - The schema version should be part of the schema location URL.">The version <em class="rfc2119">SHOULD</em> be part of the schema's location:</span></p>
<ul>
<li><span class="rule" id="OOLD-VER-befc" tabindex="0" role="link" title="OOLD-VER-befc - Under single-schema versioning, the version should be appended after the schema name in the $id.">For <strong>single-schema</strong> versioning, the version <em class="rfc2119">SHOULD</em> be appended after the schema name, e.g. <code>https://example.org/b5203131-7321-46bb-8a11-acb3d1015840.schema.json/1.1.0</code>.</span></li>
<li><span class="rule" id="OOLD-VER-4261" tabindex="0" role="link" title="OOLD-VER-4261 - Under schema-package versioning, the package version should be prepended before the schema id.">For <strong>schema-package</strong> versioning (recommended), the version of the package <em class="rfc2119">SHOULD</em> be prepended before the schema's ID, e.g. <code>https://example.org/my-package/2.0.0/b5203131-7321-46bb-8a11-acb3d1015840.schema.json</code>.</span> Since a package combines multiple schemas, the package version does in general not match the individual schema version.</li>
<li>or a release tag on a code-hosting service, e.g. <code>https://raw.githubusercontent.com/MyOrg/my-package/refs/tags/2.0.0/b5203131-7321-46bb-8a11-acb3d1015840.schema.json</code>. <span class="rule" id="OOLD-VER-d826" tabindex="0" role="link" title="OOLD-VER-d826 - A version carried in a schema location should be pinned to an immutable ref, not a mutable branch.">Such a location <em class="rfc2119">SHOULD</em> name an <strong>immutable</strong> ref: a branch name identifies a moving target, whose content changes with every push, rather than a fixed version.</span></li>
</ul>
<p><span class="rule" id="OOLD-VER-5f82" tabindex="0" role="link" title="OOLD-VER-5f82 - A version segment in a location should be recognizable as a version.">A version segment <em class="rfc2119">SHOULD</em> be recognizable as one.</span> A dotted version such as <code>2.0.0</code> already reads as a version; a bare number does not. <span class="rule" id="OOLD-VER-066b" tabindex="0" role="link" title="OOLD-VER-066b - A bare number used as a version segment should carry a v prefix.">A bare number used as a version segment <em class="rfc2119">SHOULD</em> therefore carry a <code>v</code> prefix, as in <code>.../my-package/v2/...</code>.</span> How coarsely a consumer pins - a full version, or a major-only alias that floats with compatible releases - is a policy of the package or registry that serves the schemas, not of this specification. Such a policy <em class="rfc2119">MAY</em> be expressed as a redirect chain, with <code>v1</code> resolving to the latest compatible feature release and that in turn to its latest patch, so a referrer decides how much it wants to float by choosing how much of the version it names, and a full version always remains reachable as the immutable end of the chain.</p>
<p><span class="rule" id="OOLD-VER-c92e" tabindex="0" role="link" title="OOLD-VER-c92e - A schema published for long-term reuse should be identified by a persistent identifier that resolves to its current hosting.">A raw hosting URL is convenient, but it binds the identifier to one host and one repository layout and carries no persistence guarantee, so a schema published for long-term reuse <em class="rfc2119">SHOULD</em> be identified by a <strong>persistent identifier</strong> - a <a href="https://w3id.org/">w3id.org</a> or <a href="https://purl.archive.org/">PURL</a> redirect, or a DOI for a released package - that resolves to wherever the schema is currently hosted.</span> The persistent identifier is then the <code>$id</code>, and the raw URL is only where it happens to resolve today, so the schema survives a move between hosts without changing identity.</p>
<p>Schemas <em class="rfc2119">MAY</em> indicate explicit backward-compatibility with <code>x-oold-backward-compatible-with</code> and <code>x-oold-incompatible-with</code>:</p>
<aside class="example" title="Declaring backward-compatibility"><pre><code class="language-json">{
"$id": "https://example.org/my-package/2.1.0/b5203131-7321-46bb-8a11-acb3d1015840.schema.json",
"@context": {},
"x-oold-uuid": "b5203131-7321-46bb-8a11-acb3d1015840",
"title": "Foo",
"x-oold-version": "1.1.0",
"x-oold-prior-version": "1.0.0",
"x-oold-backward-compatible-with": "https://example.org/my-package/2.0.0/b5203131-7321-46bb-8a11-acb3d1015840.schema.json",
"x-oold-incompatible-with": "https://example.org/my-package/1.0.0/b5203131-7321-46bb-8a11-acb3d1015840.schema.json"
}
</code></pre></aside>
<p>Schemas within a package or package repository <em class="rfc2119">MAY</em> use relative URIs ([[RFC3986]] §5.1). For example, <code>A.schema.json</code> referenced from <code>https://raw.githubusercontent.com/MyOrg/my-package/refs/tags/2.0.0/</code>:</p>
<aside class="example" title="Relative `$ref` inside a package"><pre><code class="language-json">{
"$id": "B.schema.json",
"@context": {},
"title": "Foo",
"allOf": [ { "$ref": "A.schema.json" } ]
}
</code></pre>
<p>expands to:</p>
<pre><code class="language-json">{
"$id": "https://raw.githubusercontent.com/MyOrg/my-package/refs/tags/2.0.0/B.schema.json",
"@context": {},
"title": "Foo",
"allOf": [ { "$ref": "https://raw.githubusercontent.com/MyOrg/my-package/refs/tags/2.0.0/A.schema.json" } ]
}
</code></pre></aside>
<p>Instance documents reference a versioned schema URL, as required in <a href="#schema-instances"></a>, to make clear which schema version they comply with:</p>
<aside class="example" title="An instance pinned to a schema version"><pre><code class="language-json">{
"@context": "https://example.org/my-package/1.0.0/b5203131-7321-46bb-8a11-acb3d1015840.schema.json",
"$schema": "https://example.org/my-package/1.0.0/b5203131-7321-46bb-8a11-acb3d1015840.schema.json"
}
</code></pre></aside>
<p>Upgrade APIs <em class="rfc2119">MAY</em> provide automated data migration between schema (package) versions, e.g. <code>https://example.org/upgrade/my-package/1.0.0...2.0.0</code>.</p></section></section>
<section id="extensions"><h2>Standard Extensions</h2><p>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.</p><section id="jsonld-extensions"><h3>JSON-LD</h3><p>OO-LD targets [[JSON-LD11]].</p><section id="processing-mode"><h4>Processing mode (<code>@version</code>)</h4><p>OO-LD composition relies on JSON-LD 1.1 features, in particular scoped contexts: a <code>$ref</code> within a <code>type: object</code> property is reflected as a property-scoped <code>@context</code> (see <a href="#composition"></a>). Such features are unavailable to a processor running in the <code>json-ld-1.0</code> processing mode.</p>
<p><span class="rule" id="OOLD-EXT-ddda" tabindex="0" role="link" title="OOLD-EXT-ddda - A generated context should declare @version 1.1 as a JSON number.">Generated OO-LD contexts <em class="rfc2119">SHOULD</em> therefore declare <code>"@version": 1.1</code> (the JSON number <code>1.1</code>, not the string <code>"1.1"</code>).</span> 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 <code>@version</code> entry determines the processing mode, it is sufficient to declare <code>"@version": 1.1</code> once in the base context of a composition (for example a root <code>Thing</code> schema).</p></section><section id="synonyms"><h4>Term mappings and synonyms (<code>x-oold-context</code>)</h4><p>A JSON-LD <code>@context</code> binds each term to a single IRI; a term given several <code>@id</code>s 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 <code>x-oold-context</code>: an object keyed by <strong>term</strong>, where each term holds a dict keyed by the <strong>synonym IRI</strong>, and each synonym is a promotable JSON-LD term-definition fragment carrying optional mapping metadata. A <em>term</em> here is any context term - most often a property, but equally a class (through the type term, see <a href="#semantic-type"></a>) or an individual (a value term under <code>@vocab</code> coercion, see <a href="#range-of-properties"></a>) - so the same machinery aliases properties, classes and individuals alike.</p>
<p>In the minimal case a term simply lists alternative IRIs, each with an empty value (<code>{}</code>):</p>
<aside class="example" title="Minimal case - a term with alias IRIs"><pre><code class="language-json">{
"@context": { "description": "schema:description" },
"x-oold-context": {
"description": {
"rdfs:comment": {},
"skos:definition": {}
}
}
}
</code></pre></aside>
<p>Here <code>description</code> (primarily <code>schema:description</code>) also maps to <code>rdfs:comment</code> and <code>skos:definition</code>. Each bare synonym IRI defaults to <code>predicate_id: skos:exactMatch</code> and inherits the primary term's coercion on promotion.</p>
<p>Each value is itself a JSON-LD term-definition fragment (<code>@type</code>, <code>@container</code>, ...) that is promotable verbatim into <code>@context</code>, optionally carrying a strippable <code>x-oold-sssom</code> block with mapping metadata:</p>
<aside class="example" title="With coercion and SSSOM metadata"><pre><code class="language-json">{
"@context": { "name": "schema:name" },
"x-oold-context": {
"name": {
"schema:name": { "x-oold-sssom": { "predicate_id": "skos:exactMatch", "confidence": 1.0 } },
"skos:prefLabel": { "x-oold-sssom": { "predicate_id": "skos:exactMatch", "confidence": 0.95 } },
"schema:alternateName": { "@container": "@set", "x-oold-sssom": { "predicate_id": "skos:closeMatch" } }
}
}
}
</code></pre></aside>
<p>Each entry is one mapping: the <strong>key</strong> is the synonym IRI (the SSSOM <code>object_id</code>), the <strong>value</strong> is a promotable JSON-LD term-definition fragment (<code>@type</code>, <code>@container</code>, ...) plus an optional <code>x-oold-sssom</code> block, and the <code>subject_id</code> is implicit - the term's primary <code>@context</code> IRI.</p>
<p><strong>Processing contract.</strong> <span class="rule" id="OOLD-EXT-7c5d" tabindex="0" role="link" title="OOLD-EXT-7c5d - A conforming mapping processor reads exactly the synonym IRI, the term-definition fragment, predicate_id and mapping_set_id from an entry.">A conforming OO-LD mapping processor <em class="rfc2119">MUST</em> read exactly four things from each entry: the <strong>synonym IRI</strong> (the key), the promotable <strong>term-definition fragment</strong>, and two <code>x-oold-sssom</code> slots - <strong><code>predicate_id</code></strong> and <strong><code>mapping_set_id</code></strong>.</span> <span class="rule" id="OOLD-EXT-adcc" tabindex="0" role="link" title="OOLD-EXT-adcc - Conformance must not depend on anything an entry carries beyond the four contract members.">Conformance <em class="rfc2119">MUST NOT</em> depend on anything else an entry carries (the rest of <code>x-oold-sssom</code>, any further fragment keys); a processor <em class="rfc2119">MAY</em> interpret such keys as its own extension, and <em class="rfc2119">MUST</em> carry them through unchanged where it rewrites an entry rather than promoting it - promotion deliberately drops the <code>x-oold-sssom</code> blocks, as Selection describes below.</span> Those two slots, over the SKOS predicate vocabulary, are the whole stable contract an implementation depends on.</p>
<p><code>predicate_id</code> is a <a href="https://www.w3.org/TR/skos-reference/">SKOS</a> mapping predicate - <code>skos:exactMatch</code> (the default when the slot is absent), <code>skos:closeMatch</code>, <code>skos:broadMatch</code>, <code>skos:narrowMatch</code> or <code>skos:relatedMatch</code> - relating the term's primary IRI (subject) to the synonym IRI (object); it decides which entries denote equivalence. <span class="rule" id="OOLD-EXT-557e" tabindex="0" role="link" title="OOLD-EXT-557e - A predicate_id is written as a full IRI or CURIE and compared by expansion to an absolute IRI.">A <code>predicate_id</code> <em class="rfc2119">MUST</em> be written as a full IRI or a CURIE and compared <strong>by expansion to an absolute IRI</strong>, the same rule the synonym keys follow, so <code>skos:exactMatch</code> and <code>http://www.w3.org/2004/02/skos/core#exactMatch</code> are one predicate.</span> <code>x-oold-context</code> is a <em>schema</em>-level keyword consumed by OO-LD processors (it is promoted into a clean <code>@context</code> before any generic JSON-LD processor runs), so its CURIEs - the synonym keys and the <code>predicate_id</code> / <code>mapping_set_id</code> values alike - are expanded not against the instance <code>@context</code> but against a fixed <strong>well-known prefix set the meta-schema defines</strong> (<code>skos</code>, <code>rdfs</code>, <code>owl</code>, <code>xsd</code>, <code>sssom</code>), reached through the schema's <code>$schema</code>. The contract therefore holds without the author redeclaring those prefixes in the data context. <span class="rule" id="OOLD-EXT-1dc8" tabindex="0" role="link" title="OOLD-EXT-1dc8 - A predicate_id must not be a bare local name.">A bare local name (<code>exactMatch</code>) <em class="rfc2119">MUST NOT</em> be used as a <code>predicate_id</code>.</span></p>
<p><code>mapping_set_id</code> names the mapping set(s) an entry belongs to, for profile-based selection. SSSOM defines <code>mapping_set_id</code> at the set level; OO-LD records it inline on the entry, and an entry <em class="rfc2119">MAY</em> belong to several sets (a mapping can appear in more than one), so its value is one <code>iri-reference</code> or an array of them, compared by expansion as <code>predicate_id</code> is.</p>
<p><strong>Selection.</strong> <span class="rule" id="OOLD-EXT-4966" tabindex="0" role="link" title="OOLD-EXT-4966 - Promotion selects one synonym per term for the target profile, writes it as the term definition and drops the x-oold-sssom blocks.">To promote <code>x-oold-context</code> into a real <code>@context</code>, a preprocessor <em class="rfc2119">MUST</em> select one synonym per term for a <strong>target profile</strong>, write <code>{ "@id": <synonym IRI>, ...fragment without x-oold-sssom }</code> as that term's definition, and drop the <code>x-oold-sssom</code> blocks, so standard JSON-LD tools then run on a clean context.</span> A profile is expressed either as an ordered list of IRI <strong>namespaces</strong> (ontology-family priority - <code>schema:</code> before <code>bfo:</code> before <code>emmo:</code>) or as one or more <strong><code>mapping_set_id</code>s</strong> (a set may span namespaces, e.g. a PMDco profile of <code>pmd:</code> plus reused <code>obo:</code> terms). A term with no synonym matching the target keeps its default <code>@context</code> IRI. <span class="rule" id="OOLD-EXT-8f62" tabindex="0" role="link" title="OOLD-EXT-8f62 - Selection must not use a synonym outside the target profile; within an ordered profile the highest-priority match wins.">Selection <em class="rfc2119">MUST NOT</em> use a synonym from outside the target profile; where the profile is an ordered list, the highest-priority match wins and a lower-priority entry is selected only where no higher one matches.</span></p>
<p><strong>Co-emission.</strong> Selection yields one IRI per term; for interoperability a converter <em class="rfc2119">MAY</em> additionally co-emit the instance value under other synonyms' IRIs. This is a pragmatic interoperability aid, not a logical entailment: <code>skos:exactMatch</code> records that two terms are interchangeable across a wide range of applications, but it is <em>not</em> <code>owl:equivalentProperty</code> / <code>owl:equivalentClass</code> and licenses no reasoner inference - which is exactly why the mapping predicates are SKOS (reasoner-safe) rather than OWL. <span class="rule" id="OOLD-EXT-7256" tabindex="0" role="link" title="OOLD-EXT-7256 - Entries that are not exactMatch should not be co-emitted unless a consumer explicitly requests it.">By default a converter co-emits only <code>skos:exactMatch</code> entries; entries whose <code>predicate_id</code> is <code>skos:closeMatch</code>/<code>broadMatch</code>/<code>narrowMatch</code>/<code>relatedMatch</code> <em class="rfc2119">SHOULD NOT</em> 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.</span></p>
<p><strong>Override under composition.</strong> The dict is keyed by IRI and resolved by the <a href="#merge-and-override-model">merge and override model</a>: most-derived-wins, with <code>null</code> removing an inherited mapping. Keys - and <code>predicate_id</code> / <code>mapping_set_id</code> values - are compared as expanded IRIs, so the compact (<code>skos:prefLabel</code>) and full (<code>http://www.w3.org/2004/02/skos/core#prefLabel</code>) forms of one IRI are a single key and override correctly across mixed notations.</p>
<aside class="example" title="A subclass dropping an inherited mapping"><pre><code class="language-json">{
"x-oold-context": {
"description": { "rdfs:comment": null }
}
}
</code></pre></aside>
<p><strong>SSSOM round-trip.</strong> Each entry is one <a href="https://w3id.org/sssom/">SSSOM</a> mapping row: <code>subject_id</code> implicit, <code>object_id</code> the synonym IRI, and <code>predicate_id</code> / <code>mapping_set_id</code> / <code>confidence</code> / <code>mapping_justification</code> / <code>object_source*</code> / ... the SSSOM slots defined by the SSSOM LinkML schema at <code>https://w3id.org/sssom/</code>. Entries sharing a <code>mapping_set_id</code> reconstitute one SSSOM mapping set; the propagatable <code>object_source</code> / <code>object_source_version</code> 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 <a href="#composition">composition</a>. SSSOM is the recommended carrier for this metadata, not a normative dependency: OO-LD fixes only the SKOS meaning of <code>predicate_id</code> and the selection role of <code>mapping_set_id</code>, 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.</p>
<p>Conversely, several context terms mapping to <strong>one</strong> IRI normalize syntactically non-interoperable input keys onto a single predicate - the complement of one term carrying several IRIs. Combined with <code>@reverse</code> and <a href="#framing">framing</a>, this turns a dataset whose records report the same relation in different ways into a consistent unified graph.</p>
<aside class="example" title="Normalizing a syntactically non-interoperable dataset"><p>Input - the same relation is reported in three ways: two forward keys (<code>works_for</code>, <code>works_for_alt</code>) mapping to one predicate, and a backward <code>employees</code> mapped with <code>@reverse</code>:</p>
<div class="ex-tabs">
<div class="ex-tablist" role="tablist"><button class="ex-tab" role="tab" aria-selected="true" data-panel="ex6j">JSON</button><button class="ex-tab" role="tab" aria-selected="false" data-panel="ex6y">View as YAML</button></div>
<div class="ex-panel" id="ex6j" role="tabpanel">
<pre><code class="language-json">{
"@graph": [
{
"id": "demo:person1",
"type": "schema:Person",
"name": "Person1",
"works_for": "demo:organizationA",
"works_for_alt": "demo:organizationB"
},
{ "id": "demo:organizationA", "type": "schema:Organization" },
{ "id": "demo:organizationB", "type": "schema:Organization" },
{
"id": "demo:organizationC",
"type": "schema:Organization",
"employees": "demo:person1"
}
]
}
</code></pre>
</div>
<div class="ex-panel" id="ex6y" role="tabpanel" hidden>
<pre><code class="language-yaml">'@graph':
- id: demo:person1
type: schema:Person
name: Person1
works_for: demo:organizationA
works_for_alt: demo:organizationB
- id: demo:organizationA
type: schema:Organization
- id: demo:organizationB
type: schema:Organization
- id: demo:organizationC
type: schema:Organization
employees: demo:person1
</code></pre>
</div>
</div>
<p>Normalized - a single, consistent representation:</p>
<div class="ex-tabs">
<div class="ex-tablist" role="tablist"><button class="ex-tab" role="tab" aria-selected="true" data-panel="ex7j">JSON</button><button class="ex-tab" role="tab" aria-selected="false" data-panel="ex7y">View as YAML</button></div>
<div class="ex-panel" id="ex7j" role="tabpanel">
<pre><code class="language-json">{
"@graph": [
{
"id": "demo:organizationA",
"type": "schema:Organization",
"employees": ["demo:person1", "demo:person2", "demo:person3"],
"label": [ { "lang": "en", "text": "organizationA" } ]
},
{ "id": "demo:person1", "type": "schema:Person", "name": "Person1" },
{ "id": "demo:person2", "type": "schema:Person", "name": "Person2" },
{ "id": "demo:person3", "type": "schema:Person", "name": "Person3" }
]
}
</code></pre>
</div>
<div class="ex-panel" id="ex7y" role="tabpanel" hidden>
<pre><code class="language-yaml">'@graph':
- id: demo:organizationA
type: schema:Organization
employees:
- demo:person1
- demo:person2
- demo:person3
label:
- lang: en
text: organizationA
- id: demo:person1
type: schema:Person
name: Person1
- id: demo:person2
type: schema:Person
name: Person2
- id: demo:person3
type: schema:Person
name: Person3
</code></pre>
</div>
</div></aside></section><section id="framing"><h4>Framing</h4><p><a href="https://www.w3.org/TR/json-ld11-framing/">JSON-LD 1.1 Framing</a> reshapes a flat or arbitrarily-structured RDF graph into a specific tree layout described by a <em>frame</em>. An OO-LD schema already describes exactly such a tree - its <code>properties</code> give the nesting, its <code>@context</code> gives the term IRIs, a type constant (<code>x-oold-instance-rdf-type</code>, or a <code>const</code> on the <code>type</code> property) gives the node type, and <a href="#range-of-properties"><code>x-oold-range</code></a> gives the type of embedded or referenced objects - so an OO-LD-aware tool <em class="rfc2119">MAY</em> 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.</p>
<p>This makes an OO-LD schema bidirectional: its <code>@context</code> drives expansion (JSON to RDF), and the frame derived from its structure drives framing (RDF back to the schema's JSON tree). <span class="rule" id="OOLD-EXT-68fa" tabindex="0" role="link" title="OOLD-EXT-68fa - A frame derived from a schema takes its @type from the class type, embeds inlined object properties as subframes and keeps reference-valued properties as IRIs with @embed @never.">A tool that derives a frame <em class="rfc2119">MUST</em> derive it mechanically: the schema's class type becomes the frame <code>@type</code>; 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 <code>@reverse</code>) becomes a subframe with <code>@embed: @never</code> so its targets stay IRIs, in line with the inline-versus-reference choice <code>x-oold-range</code> already records; and <code>@explicit</code> / <code>@requireAll</code> / <code>@default</code> follow from <code>additionalProperties</code> and <code>required</code>.</span> The frame's <code>@context</code> is the composition of the referenced schemas' contexts.</p>
<p><span class="rule" id="OOLD-EXT-6d10" tabindex="0" role="link" title="OOLD-EXT-6d10 - A frame derivation recognizes a property as reference-valued from x-oold-range, an IRI-family format, a term mapped @type @id, or a term mapped @reverse.">A derivation <em class="rfc2119">MUST</em> recognize a property as <strong>reference-valued</strong> from any of four signals: an <a href="#range-of-properties"><code>x-oold-range</code></a> on a string-typed value, an IRI-family <code>format</code> (the family <a href="#range-reference-form"></a> recommends), a <code>@context</code> term mapped <code>"@type": "@id"</code>, or a <code>@context</code> term mapped with <code>@reverse</code>.</span> The last stands on its own: the values of a reverse term are node references by definition ([[JSON-LD11]] §4.1.10), so <code>"@type": "@id"</code> alongside it is redundant and an author who omits it has still written a reference. <span class="rule" id="OOLD-EXT-ff64" tabindex="0" role="link" title="OOLD-EXT-ff64 - Where a property matches both the embedded-object and the reference-valued signals, the embedded-object reading wins.">Where a property matches both a reference-valued signal and the embedded-object shape, the embedded-object reading <em class="rfc2119">MUST</em> win: a property whose value is an object is an embed whatever its term declares.</span> <span class="rule" id="OOLD-EXT-05d3" tabindex="0" role="link" title="OOLD-EXT-05d3 - A frame derivation must not constrain a property whose key aliases a JSON-LD keyword.">A derivation <em class="rfc2119">MUST NOT</em> emit a <em>constraining</em> subframe - one carrying <code>@embed</code> or any other frame keyword - for a property whose key aliases a JSON-LD keyword, conventionally <code>id</code> for <code>@id</code> and <code>type</code> for <code>@type</code> (see <a href="#identity"></a>).</span> Such a key names the node rather than pointing at another one. An empty subframe there is the frame-matching wildcard and is accepted; <code>{"@id": {"@embed": "@never"}}</code> is not, and a processor rejects the whole frame rather than the one entry. <span class="rule" id="OOLD-EXT-5ea6" tabindex="0" role="link" title="OOLD-EXT-5ea6 - A key aliasing a JSON-LD keyword is never reference-valued, whichever reference signals it carries.">A derivation <em class="rfc2119">MUST</em> apply this exclusion ahead of the reference-valued signals, so a key aliasing a JSON-LD keyword is never treated as reference-valued, however many signals it carries.</span> The two overlap in practice, because an aliased <code>id</code> is conventionally declared <code>"format": "iri"</code>, which is one of the signals.</p>
<p>Omitting <code>@embed: @never</code> is not a cosmetic difference. A reference whose target happens to carry triples in the same graph is then embedded as an object, and the framed document no longer validates against the schema the frame was derived from, which declares a string in that position.</p>
<aside class="example" title="RDF to OO-LD schema to frame to instance"><p>An input RDF graph (Turtle) - an organization with an address, and two persons who work for it:</p>
<pre><code class="language-turtle">@prefix schema: <http://schema.org/> .
@prefix ex: <https://example.org/> .
ex:org1 a schema:Organization ; schema:address ex:addr1 .
ex:addr1 a schema:PostalAddress ; schema:postalCode "10115" .
ex:p1 a schema:Person ; schema:worksFor ex:org1 .
ex:p2 a schema:Person ; schema:worksFor ex:org1 .
</code></pre>
<p>The OO-LD schema for <code>Organization</code> - <code>address</code> is an inlined object (<code>$ref</code>, reflected as a scoped <code>@context</code>), and <code>employees</code> is a regular property whose <code>@context</code> term is mapped with JSON-LD <code>@reverse</code> to the persons' <code>schema:worksFor</code>, so listing an employee here yields a <code>worksFor</code> triple on that person:</p>
<pre><code class="language-json">{
"@context": {
"schema": "http://schema.org/",
"type": "@type",
"id": "@id",
"address": { "@id": "schema:address", "@context": "Address.schema.json" },
"employees": { "@reverse": "schema:worksFor", "@type": "@id" }
},
"$id": "Organization.schema.json",
"x-oold-instance-rdf-type": ["schema:Organization"],
"type": "object",
"properties": {
"address": { "$ref": "Address.schema.json" },
"employees": {
"type": "array",
"items": { "type": "string", "x-oold-range": "Person.schema.json" }
}
}
}
</code></pre>
<p>referencing minimal <code>Address</code> and <code>Person</code> stubs:</p>
<pre><code class="language-json">{
"@context": { "schema": "http://schema.org/", "type": "@type", "id": "@id", "postalCode": "schema:postalCode" },
"$id": "Address.schema.json",
"x-oold-instance-rdf-type": ["schema:PostalAddress"],
"type": "object",
"properties": { "postalCode": { "type": "string" } }
}
</code></pre>
<pre><code class="language-json">{
"@context": { "schema": "http://schema.org/", "type": "@type" },
"$id": "Person.schema.json",
"x-oold-instance-rdf-type": ["schema:Person"],
"type": "object"
}
</code></pre>
<p>(This uses <code>@reverse</code> on an ordinary property - a read projection. The editor-only <a href="#reverse-properties"><code>x-oold-reverse-properties</code></a> affordance is different: it lets a user edit <code>employees</code> from the <code>Organization</code> while the relation is stored on the <code>Person</code> objects.)</p>
<p>The frame derived from that schema reuses the schema as its <code>@context</code>, embeds the address, and keeps employees as IRIs:</p>
<pre><code class="language-json">{
"@context": "Organization.schema.json",
"type": "schema:Organization",
"address": { "type": "schema:PostalAddress" },
"employees": { "@embed": "@never" }
}
</code></pre>
<p>Framing the graph with that frame yields an OO-LD instance document, projected onto <code>ex:org1</code> with the <code>Address</code> inlined and the persons listed as <code>employees</code> IRIs via <code>@reverse</code>. Like any OO-LD instance it references its schema for both semantics (<code>@context</code>) and validation (<code>$schema</code>):</p>
<pre><code class="language-json">{
"@context": ["Organization.schema.json", { "ex": "https://example.org/" }],
"$schema": "Organization.schema.json",
"id": "ex:org1",
"type": "schema:Organization",
"address": {
"id": "ex:addr1",