Problem
The meaning of x-gts-ref: "/$id" needs a separate specification decision, independently of splitting x-gts-ref into x-gts-type-ref and x-gts-instance-ref in #96.
README §9.6 currently describes equality with the current schema's $id, with gts:// removed. This leaves two questions that matter for inherited constraints:
- Does
/$id refer to the document where the constraint was authored, or the leaf schema selected for validation?
- Does the resolved ID require exact equality, or define a root that also permits derived types and instances?
Existing implementations already differ in whether they enforce this constraint through schema composition. This issue proposes an explicit contract and shared conformance cases. The proposed behavior below is a specification change/clarification, not a claim that the current equality wording already requires it.
Concrete example
Register these three schemas. invoice and credit_note are siblings derived from document; the authored $ref imports the base constraint.
document
├── invoice ← selected validation schema
└── credit_note
Base schema: document
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "gts://gts.x.demo._.document.v1~",
"type": "object",
"required": ["entityRef"],
"properties": {
"entityRef": {
"type": "string",
"x-gts-ref": "/$id"
}
},
"additionalProperties": false
}
Derived schema: invoice
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "gts://gts.x.demo._.document.v1~x.demo._.invoice.v1~",
"type": "object",
"allOf": [
{ "$ref": "gts://gts.x.demo._.document.v1~" }
]
}
Sibling schema: credit_note
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "gts://gts.x.demo._.document.v1~x.demo._.credit_note.v1~",
"type": "object",
"allOf": [
{ "$ref": "gts://gts.x.demo._.document.v1~" }
]
}
Also assume an invoice instance is available in the registry under this ID:
gts.x.demo._.document.v1~x.demo._.invoice.v1~x.demo._.invoice_001.v1
Its payload is valid against invoice under the proposed semantics:
{
"entityRef": "gts.x.demo._.document.v1~x.demo._.invoice.v1~"
}
The examples show payloads only. The selected schema and registry identity are validation/registration context, not required payload fields. How an API supplies that context is outside this issue.
Validate each of the following payloads against invoice:
✅ Reference to the invoice type:
{
"entityRef": "gts.x.demo._.document.v1~x.demo._.invoice.v1~"
}
✅ Reference to an invoice instance:
{
"entityRef": "gts.x.demo._.document.v1~x.demo._.invoice.v1~x.demo._.invoice_001.v1"
}
❌ Reference to the base type:
{
"entityRef": "gts.x.demo._.document.v1~"
}
❌ Reference to the sibling type:
{
"entityRef": "gts.x.demo._.document.v1~x.demo._.credit_note.v1~"
}
These marks describe the proposal: the inherited operand rebinds to invoice, which becomes the root of permitted references. All valid reference targets above are assumed registered, so the negative cases concern matching, not missing registry entries.
Observed implementation differences
The following results come from executable local probes against these revisions, not just code inspection:
For the inherited constraint via an external $ref inside allOf:
| Reference value |
Proposed result |
Rust |
Python |
Go |
TypeScript |
Selected type (invoice) |
✅ |
✅ |
✅ |
✅ |
✅ |
Instance of selected type (invoice_001) |
✅ |
✅ |
✅ |
✅ |
✅ |
Base type (document) |
❌ |
❌ |
❌ |
✅ ⚠️ |
✅ ⚠️ |
Sibling type (credit_note) |
❌ |
❌ |
❌ |
✅ ⚠️ |
✅ ⚠️ |
Invalid identifier (not-a-gts-id) |
❌ |
❌ |
❌ |
✅ ⚠️ |
✅ ⚠️ |
Here ✅ means accepted and ❌ means rejected. ⚠️ means the inherited constraint is skipped, not that the implementation deliberately chose base-rooted matching. Acceptance of even not-a-gts-id makes that distinction visible.
Additional controls locate the difference:
Where the /$id constraint is declared |
Rust |
Python |
Go |
TypeScript |
Directly in the selected schema's properties |
Enforced |
Enforced |
Enforced |
Enforced |
In an inline object subschema within allOf |
Enforced, outer root |
Enforced, outer root |
Skipped |
Enforced, outer root |
In a base schema reached through external $ref within allOf |
Enforced, leaf root |
Enforced, leaf root |
Skipped |
Skipped |
In the direct case all four accept both the selected type and its instance, so none of those tested paths implements strict equality with $id.
Relevant code:
- Rust resolves schema references before running the GTS reference validation pass: store.rs.
- Python likewise supplies the resolved schema to its reference validator: store.py.
- Go supplies the original schema to the separate pass, whose instance traversal handles properties/items but does not traverse
allOf or $ref: validate.go, x_gts_ref.go.
- TypeScript supplies the original schema; its instance traversal handles inline
allOf but does not follow $ref: store.ts, x-gts-ref.ts.
Test scope: the probes used equivalent base/child/sibling identifiers in an x.probe namespace and a field named ref. The documentation example above renames them for readability and adds required/additionalProperties constraints. Store-based probes used identity fields for registration with schemas permitting those fields; Rust passed the payload and type ID directly. These were library-level instance-validation checks, not HTTP conformance tests or a claim about all JSON Schema composition forms. Negative Rust/Python results were reference-matching or invalid-GTS-ID errors.
Proposed specification wording
Add explicit resolution and matching rules to README §9.6:
For instance validation, /$id resolves to the canonical top-level $id of the leaf GTS Type Schema selected for validation, with the gts:// prefix removed. This root remains the same when evaluating constraints inherited through explicitly authored schema references and allOf composition.
The resolved identifier is used as a GTS reference constraint under the same rooted matching rules as a literal operand. It does not imply exact equality: the selected type, its derived types, and instances rooted at those types may match. An ancestor or sibling type does not match. A schema requiring one exact value can use JSON Schema const.
Use a literal base ID when a reference must stay rooted at document regardless of the selected derived schema. A chained GTS $id alone does not import the ancestor's schema body; the inheritance example above explicitly uses $ref.
The candidate-kind restrictions proposed in #96 are a separate layer: x-gts-type-ref would allow matching types, while x-gts-instance-ref would allow matching instances. Both would reuse the resolution rules decided here.
Work to be done
- Agree on and document the root and matching rules in §9.6.
- Update existing conformance tests and add new ones to enforce the agreed
/$id semantics. Cover direct declarations, inline allOf, and external $ref inheritance, including type/instance acceptance and ancestor/sibling/invalid-ID rejection. Negative tests must check that rejection is caused by the reference constraint, rather than an unrelated registration or payload error.
- Cover a further derived type and its instances, and verify that a chained ID without an authored reference does not implicitly import constraints.
- Align the four implementations with the agreed contract.
Registry-existence policy and the keyword split remain separate from the decision in this issue.
Problem
The meaning of
x-gts-ref: "/$id"needs a separate specification decision, independently of splittingx-gts-refintox-gts-type-refandx-gts-instance-refin #96.README §9.6 currently describes equality with the current schema's
$id, withgts://removed. This leaves two questions that matter for inherited constraints:/$idrefer to the document where the constraint was authored, or the leaf schema selected for validation?Existing implementations already differ in whether they enforce this constraint through schema composition. This issue proposes an explicit contract and shared conformance cases. The proposed behavior below is a specification change/clarification, not a claim that the current equality wording already requires it.
Concrete example
Register these three schemas.
invoiceandcredit_noteare siblings derived fromdocument; the authored$refimports the base constraint.Base schema:
document{ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "gts://gts.x.demo._.document.v1~", "type": "object", "required": ["entityRef"], "properties": { "entityRef": { "type": "string", "x-gts-ref": "/$id" } }, "additionalProperties": false }Derived schema:
invoice{ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "gts://gts.x.demo._.document.v1~x.demo._.invoice.v1~", "type": "object", "allOf": [ { "$ref": "gts://gts.x.demo._.document.v1~" } ] }Sibling schema:
credit_note{ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "gts://gts.x.demo._.document.v1~x.demo._.credit_note.v1~", "type": "object", "allOf": [ { "$ref": "gts://gts.x.demo._.document.v1~" } ] }Also assume an invoice instance is available in the registry under this ID:
Its payload is valid against
invoiceunder the proposed semantics:{ "entityRef": "gts.x.demo._.document.v1~x.demo._.invoice.v1~" }The examples show payloads only. The selected schema and registry identity are validation/registration context, not required payload fields. How an API supplies that context is outside this issue.
Validate each of the following payloads against
invoice:✅ Reference to the invoice type:
{ "entityRef": "gts.x.demo._.document.v1~x.demo._.invoice.v1~" }✅ Reference to an invoice instance:
{ "entityRef": "gts.x.demo._.document.v1~x.demo._.invoice.v1~x.demo._.invoice_001.v1" }❌ Reference to the base type:
{ "entityRef": "gts.x.demo._.document.v1~" }❌ Reference to the sibling type:
{ "entityRef": "gts.x.demo._.document.v1~x.demo._.credit_note.v1~" }These marks describe the proposal: the inherited operand rebinds to
invoice, which becomes the root of permitted references. All valid reference targets above are assumed registered, so the negative cases concern matching, not missing registry entries.Observed implementation differences
The following results come from executable local probes against these revisions, not just code inspection:
GtsStore::validate_payloadGtsStore.validate_instance, which delegates tovalidate_instance_contentGtsStore.ValidateInstanceGtsStore.validateInstanceFor the inherited constraint via an external
$refinsideallOf:invoice)invoice_001)document)credit_note)not-a-gts-id)Here ✅ means accepted and ❌ means rejected.⚠️ means the inherited constraint is skipped, not that the implementation deliberately chose base-rooted matching. Acceptance of even
not-a-gts-idmakes that distinction visible.Additional controls locate the difference:
/$idconstraint is declaredpropertiesallOf$refwithinallOfIn the direct case all four accept both the selected type and its instance, so none of those tested paths implements strict equality with
$id.Relevant code:
allOfor$ref: validate.go, x_gts_ref.go.allOfbut does not follow$ref: store.ts, x-gts-ref.ts.Test scope: the probes used equivalent
base/child/siblingidentifiers in anx.probenamespace and a field namedref. The documentation example above renames them for readability and addsrequired/additionalPropertiesconstraints. Store-based probes used identity fields for registration with schemas permitting those fields; Rust passed the payload and type ID directly. These were library-level instance-validation checks, not HTTP conformance tests or a claim about all JSON Schema composition forms. Negative Rust/Python results were reference-matching or invalid-GTS-ID errors.Proposed specification wording
Add explicit resolution and matching rules to README §9.6:
Use a literal base ID when a reference must stay rooted at
documentregardless of the selected derived schema. A chained GTS$idalone does not import the ancestor's schema body; the inheritance example above explicitly uses$ref.The candidate-kind restrictions proposed in #96 are a separate layer:
x-gts-type-refwould allow matching types, whilex-gts-instance-refwould allow matching instances. Both would reuse the resolution rules decided here.Work to be done
/$idsemantics. Cover direct declarations, inlineallOf, and external$refinheritance, including type/instance acceptance and ancestor/sibling/invalid-ID rejection. Negative tests must check that rejection is caused by the reference constraint, rather than an unrelated registration or payload error.Registry-existence policy and the keyword split remain separate from the decision in this issue.