Skip to content

Clarify /$id resolution and matching semantics in inherited schemas #109

Description

@aviator5

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:

  1. Does /$id refer to the document where the constraint was authored, or the leaf schema selected for validation?
  2. 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:

Library Tested revision Validation entry point
gts-rust 1b2df023 GtsStore::validate_payload
gts-python f563083b GtsStore.validate_instance, which delegates to validate_instance_content
gts-go 43f42753 GtsStore.ValidateInstance
gts-ts 8b40c9ec GtsStore.validateInstance

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions