From 3c92b967db58295ffeeacebbadde68dc81e870eb Mon Sep 17 00:00:00 2001 From: Artifizer Date: Mon, 22 Dec 2025 20:52:46 +0200 Subject: [PATCH 1/4] doc(entities): strict schema/instance distinction based on $schema field BREAKING CHANGE: Entity classification now strictly follows the rule that a JSON document is a schema if and only if it has a "$schema" field. This commit implements a clear distinction between 5 categories of GTS entities: 1. **GTS Schemas** - Have `$schema` field + `$id` with `gts://...~` format 2. **Non-GTS Schemas** - Have `$schema` but no valid GTS `$id` (unsupported) 3. **Well-Known Instances** - No `$schema`, `id` field contains GTS ID with chain 4. **Anonymous Instances** - No `$schema`, `id` field contains UUID, `type` field has GTS schema ID 5. **Unknown Instances** - No `$schema`, no identifiable schema (unsupported) - **Before**: Schema detection used multiple heuristics (ID ending with `~`, `$id` field, `$schema` URL patterns) - **After**: A document is a schema **if and only if** it has a `$schema` field --- README.md | 133 ++++++++++++++++++- tests/test_op2_id_extraction.py | 218 ++++++++++++++++++++++++++++++-- 2 files changed, 339 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 0f59d91..7d2fbaf 100644 --- a/README.md +++ b/README.md @@ -456,6 +456,16 @@ However, an **instance/object** may be represented in two common ways: - `gts.x.core.events.topic.v1~x.commerce._.orders.v1.0` - Field naming: typically `id` (alternatives: `gtsId`, `gts_id`). +Example: + +```json +{ + "id": "gts.x.core.events.topic.v1~x.commerce._.orders.v1.0", + "name": "orders", + "description": "Order lifecycle events topic" +} +``` + - **Anonymous instance**: used for runtime-created objects where a globally meaningful name is not required (events/messages, DB rows, audit records, etc.). - Recommended: use an opaque identifier as `id` (typically a UUID) and store the associated GTS **type identifier** separately (e.g., in a `type` field). - Example (anonymous event instance): @@ -464,6 +474,15 @@ However, an **instance/object** may be represented in two common ways: This split is common in event systems: **topics/streams** are often well-known instances, while individual **events** are anonymous. See `./examples/events` and the field-level recommendations in section **9.1**. +Example: + +```json +{ + "id": "7a1d2f34-5678-49ab-9012-abcdef123456", + "type": "gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~", + "occurredAt": "2025-09-20T18:35:00Z" +} +``` ## 4. GTS Identifier Versions Compatibility @@ -1276,7 +1295,119 @@ Result: ❌ NO MATCH (different major versions) ## 11. JSON and JSON Schema Conventions -It is advisable to include instance GTS identifiers in a top-level field, such as `id`. However, the choice of the specific field name is left to the discretion of the implementation and can vary from service to service. +### 11.1 Global rules: schema vs instance, normalization, and document categories + +This section defines recommendations for how GTS-aware systems interpret JSON documents. The rules describe the concepts; the exact field names used for instance IDs and instance types are **implementation-defined** and may be **configuration-driven** (different systems may look for identifiers in different fields). + +#### Rule A — Schema vs instance discriminator + +**A JSON document is a schema if and only if it contains a top-level `$schema` field.** + +- If `$schema` is present → the document MUST be treated as a **schema**. +- If `$schema` is absent → the document MUST be treated as an **instance**. + +This discriminator MUST be applied before any ID parsing heuristics. + +#### Rule B — GTS schema `$id` normalization + +For GTS schemas (documents with `$schema`), it is recommended that `$id` is URI-compatible by using: +- `$id: "gts://"` + +Implementations MUST normalize this by stripping the `gts://` prefix when extracting/returning the canonical GTS identifier. The `gts://` prefix exists only to make `$id` URI-compatible. + +#### Rule C — JSON document categories + +Implementations MUST clearly distinguish the following **five** categories of JSON documents: + +1. **GTS entity schemas** + - Have `$schema` + - Have `$id` starting with `gts://` and the remainder is a valid **GTS type identifier** (ends with `~`) + - Example: + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "gts://gts.x.core.events.type.v1~", + "type": "object" +} +``` + +2. **Non‑GTS schemas** + - Have `$schema` + - Do not have a valid GTS `$id` + - Handling is **implementation-defined** (ignore vs error depending on API context) + - Example: + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/schemas/order.json", + "type": "object" +} +``` + +3. **Instances of unknown / non‑GTS schemas** + - No `$schema` + - Schema/type cannot be determined (no acceptable schema/type reference field found, or the field value is not a valid GTS ID) + - Handling is **implementation-defined** (ignore vs error depending on API context) + - Example: + +```json +{ + "id": "123", + "payload": { "foo": "bar" } +} +``` + +4. **Well-known GTS instances (named)** + - No `$schema` + - Instance is identified by a **GTS instance identifier** (often a chain) stored in an implementation-chosen instance-ID field + - The schema/type is derived from the **left segment(s)** of the chain + - Example (well-known topic/stream instance): + +```json +{ + "id": "gts.x.core.events.topic.v1~x.commerce._.orders.v1.0", + "name": "orders" +} +``` + +> NOTE: In this specification, an instance identifier is a GTS identifier **without** the trailing `~` (i.e., it does not name a schema/type). +> Some systems may still accept an `id` field or it's equivalent that contains a **type/schema** identifier (ending with `~`) and treat it as a *schema reference* rather than an *instance identifier*. +> This behavior is **not defined by the GTS spec** and is entirely **implementation-specific / configuration-driven**. + +5. **Anonymous GTS instances** + - No `$schema` + - Instance `id` is opaque (typically UUID) + - Schema/type is provided separately via an implementation-chosen schema/type field (e.g., `type`, `gtsType`, `gts_type`) + - Example (anonymous event instance): + +```json +{ + "id": "7a1d2f34-5678-49ab-9012-abcdef123456", + "type": "gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~", + "occurredAt": "2025-09-20T18:35:00Z" +} +``` + +> NOTE: In this specification, a type identifier is a GTS identifier **with** the trailing `~`. +> Some systems may still accept a `type` field or it's equivalent that contains an **instance** identifier (not ending with `~`). This behavior is **not defined by the GTS spec** and is entirely **implementation-specific / configuration-driven**. + + +#### ID and type-field heuristics (implementation-defined) + +For **instances** (documents without `$schema`), implementations typically apply heuristics in this order: + +1. **Try instance ID fields** (commonly `id`, then aliases like `gtsId`, `gts_id`): + - If the value is a valid GTS identifier, treat it as a **well-known instance** and derive `schema_id` from the chain (everything up to and including the last `~`). + - Otherwise treat it as an **anonymous instance** ID value. +2. **For anonymous instances**, determine the schema/type from a separate field (commonly `type`, or aliases like `schema`, `gtsType`, `gts_type`). + +Different systems may choose different field names and priority orders via configuration. The examples below (and the `./examples/*` folders) use the common defaults: `id` for instance ID and `type` for instance type. + +### 11.2 Examples + +It is advisable to include instance identifiers in a top-level field such as `id`. However, the choice of the specific field name is left to the discretion of the implementation and can vary from service to service. **Example #1**: **instance definition** of an object instance (event topic) that has an `id` field that encodes the object type (`gts.x.core.events.topic.v1~`) and identifies the object itself (`x.core.idp.events.v1`). In the example below it makes no sense to add an additional `type` field referring to the object schema because the `id` is already unique and there are no other event topics with the given id in the system: diff --git a/tests/test_op2_id_extraction.py b/tests/test_op2_id_extraction.py index 4605bb3..2d0d311 100644 --- a/tests/test_op2_id_extraction.py +++ b/tests/test_op2_id_extraction.py @@ -1,4 +1,4 @@ -import pytest +import requests from .conftest import get_gts_base_url from httprunner import HttpRunner, Config, Step, RunRequest @@ -68,13 +68,21 @@ def test_start(self): Step( RunRequest("extract id (case 3)") .post("/extract-id") - .with_json({"id": "gts.v123.p456.n789.t000.v999.888~"}) + .with_json({ + "id": "gts.x.core.events.topic.v1~x.commerce._.orders.v1.0" + }) .validate() .assert_equal("status_code", 200) - .assert_equal("body.id", "gts.v123.p456.n789.t000.v999.888~") - .assert_equal("body.schema_id", "gts.v123.p456.n789.t000.v999.888~") + .assert_equal( + "body.id", + "gts.x.core.events.topic.v1~x.commerce._.orders.v1.0", + ) + .assert_equal( + "body.schema_id", + "gts.x.core.events.topic.v1~", + ) .assert_equal("body.selected_entity_field", "id") - .assert_equal("body.selected_schema_id_field", None) + .assert_equal("body.selected_schema_id_field", "id") .assert_equal("body.is_schema", False) ), ] @@ -91,20 +99,208 @@ def test_start(self): RunRequest("extract id (case 4)") .post("/extract-id") .with_json({ - "gts_id": "gts.x.test2.objects_registry.object_a.v1.0", - "schema": "https://json-schema.org/draft/2020-12/schema", + "id": "7a1d2f34-5678-49ab-9012-abcdef123456", + "type": ( + "gts.x.core.events.type.v1~" + "x.commerce.orders.order_placed.v1.0~" + ), }) .validate() .assert_equal("status_code", 200) .assert_equal( - "body.id", "gts.x.test2.objects_registry.object_a.v1.0" + "body.id", + "7a1d2f34-5678-49ab-9012-abcdef123456", ) .assert_equal( "body.schema_id", - "https://json-schema.org/draft/2020-12/schema", + ( + "gts.x.core.events.type.v1~" + "x.commerce.orders.order_placed.v1.0~" + ), ) - .assert_equal("body.selected_entity_field", "gts_id") - .assert_equal("body.selected_schema_id_field", "schema") + .assert_equal("body.selected_entity_field", "id") + .assert_equal("body.selected_schema_id_field", "type") .assert_equal("body.is_schema", False) ), ] + + +class TestCaseTestOp2IdExtraction_Case5_GtsBaseSchema(HttpRunner): + """ + Schemas MUST be detected by presence of $schema; GTS $id MUST be normalized + by stripping gts://. + """ + config = Config("OP#2 - Extract ID (case 5: GTS base schema)").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + Step( + RunRequest("extract id for base schema") + .post("/extract-id") + .with_json({ + "$$schema": "http://json-schema.org/draft-07/schema#", + "$$id": "gts://gts.x.core.events.type.v1~", + "type": "object", + }) + .validate() + .assert_equal("status_code", 200) + # NOTE: + # - These two cases send JSON Schema keys ($schema / $id). + # - HttpRunner/httprunner uses "$..." for variable/template syntax. + # so $-prefixed keys can get interpolated and be brittle here. + # - Therefore this class is only a smoke test: + # we only check the server classifies the input as a schema. + # - The real contract checks are implemented below + # (plain pytest + `requests`). + .assert_equal("body.is_schema", True) + ), + ] + + +class TestCaseTestOp2IdExtraction_Case6_GtsDerivedSchema(HttpRunner): + """ + For derived schemas, schema_id is derived from the $id chain + (not taken from $schema). + """ + config = Config("OP#2 - Extract ID (case 6: GTS derived schema)").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + Step( + RunRequest("extract id for derived schema") + .post("/extract-id") + .with_json({ + "$$schema": "http://json-schema.org/draft-07/schema#", + "$$id": ( + "gts://gts.x.core.events.type.v1~" + "x.commerce.orders.order_placed.v1.0~" + ), + "type": "object", + }) + .validate() + .assert_equal("status_code", 200) + # NOTE: + # - These two cases send JSON Schema keys ($schema / $id). + # - HttpRunner/httprunner uses "$..." for variable/template syntax. + # so $-prefixed keys can get interpolated and be brittle here. + # - Therefore this class is only a smoke test: + # we only check the server classifies the input as a schema. + # - The real contract checks are implemented below + # (plain pytest + `requests`). + .assert_equal("body.is_schema", True) + ), + ] + + +def test_op2_extract_id_gts_base_schema_normalizes_id() -> None: + """Schema detection uses $schema; gts:// prefix is stripped from $id.""" + url = get_gts_base_url() + "/extract-id" + payload = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "gts://gts.x.core.events.type.v1~", + "type": "object", + } + r = requests.post(url, json=payload, timeout=30) + assert r.status_code == 200 + body = r.json() + assert body["is_schema"] is True + assert body["id"] == "gts.x.core.events.type.v1~" + assert body["schema_id"] == "http://json-schema.org/draft-07/schema#" + + +def test_op2_extract_id_gts_derived_schema_parent_from_chain() -> None: + """For derived schemas, schema_id is derived from the $id chain.""" + url = get_gts_base_url() + "/extract-id" + payload = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": ( + "gts://gts.x.core.events.type.v1~" + "x.commerce.orders.order_placed.v1.0~" + ), + "type": "object", + } + r = requests.post(url, json=payload, timeout=30) + assert r.status_code == 200 + body = r.json() + assert body["is_schema"] is True + assert ( + body["id"] + == "gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~" + ) + assert body["schema_id"] == "gts.x.core.events.type.v1~" + + +def test_op2_extract_id_schema_without_id_is_non_gts_schema() -> None: + """ + If $schema is present but $id is missing, the document is a schema + (Rule A) but it is NOT a GTS schema (Rule C #2). + """ + url = get_gts_base_url() + "/extract-id" + payload = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + } + r = requests.post(url, json=payload, timeout=30) + assert r.status_code == 200 + body = r.json() + assert body["is_schema"] is True + # Not a GTS schema: no canonical gts.* id can be extracted. + extracted_id = body.get("id") + assert extracted_id in (None, "") or not str(extracted_id).startswith( + "gts." + ) + # Without a GTS $id chain, schema_id falls back to $schema. + assert body.get("schema_id") == payload["$schema"] + + +def test_op2_extract_id_id_without_schema_is_not_a_gts_schema() -> None: + """ + If $id is present but $schema is missing, the document is an instance + (Rule A). The '$id' field value is a GTS identifier (not a GTS schema). + """ + url = get_gts_base_url() + "/extract-id" + payload = { + "id": "7a1d2f34-5678-49ab-9012-abcdef123456", + "$id": "gts://gts.x.core.events.test_type.v10~", + } + r = requests.post(url, json=payload, timeout=30) + assert r.status_code == 200 + body = r.json() + assert body["is_schema"] is False + assert body["id"] == payload["$id"].replace("gts://", "") + # No type field => schema_id should not be inferred from $id. + assert body.get("schema_id") is None + + +def test_op2_extract_id_uuid_without_type_is_non_gts_instance() -> None: + """ + UUID id without type/schema reference is a non-GTS instance (Rule C #3). + """ + url = get_gts_base_url() + "/extract-id" + payload = {"id": "7a1d2f34-5678-49ab-9012-abcdef123456"} + r = requests.post(url, json=payload, timeout=30) + assert r.status_code == 200 + body = r.json() + assert body["is_schema"] is False + assert body["id"] == payload["id"] + assert body.get("schema_id") is None + + +def test_op2_extract_id_non_gts_id_is_non_gts_instance() -> None: + """Non-GTS id value without type/schema reference is a non-GTS instance.""" + url = get_gts_base_url() + "/extract-id" + payload = {"id": "not-a-gts-id"} + r = requests.post(url, json=payload, timeout=30) + assert r.status_code == 200 + body = r.json() + assert body["is_schema"] is False + assert body["id"] == payload["id"] + assert body.get("schema_id") is None From 8914e51b50b0de6b7ec625184962120cff1b1c62 Mon Sep 17 00:00:00 2001 From: Artifizer Date: Mon, 22 Dec 2025 22:06:48 +0200 Subject: [PATCH 2/4] docs: recommend gts:// in JSON Schema $ref; tests: enforce invalid $ref errors Document that JSON Schema $ref should be URI-compatible with gts://, same as $id, and trimmed back to canonical gts.a.b.c.d... for resolution/registry keys. Update OP#7 relationship-resolution fixtures to use gts:// for all schema refs. Add OP#7 tests asserting POST /entities?validate=true returns 422 for: $ref that is not gts://... $ref that has gts:// but contains a malformed GTS identifier Signed-off-by: Artifizer --- README.md | 32 +++-- ...erce.orders.order_placed.v1.0~.schema.json | 2 +- ...erce.orders.order_placed.v1.1~.schema.json | 2 +- ...x.core.idp.contact_created.v1~.schema.json | 2 +- ...core.idp.billing_contact.v1.0~.schema.json | 2 +- ...v1~x.ai.mcp.http_outbound.v1~.schema.jsonc | 2 +- tests/test_op10_query_execution.py | 4 +- tests/test_op11_attribute_access.py | 4 +- tests/test_op6_schema_validation.py | 4 +- tests/test_op7_relationship_resolution.py | 135 ++++++++++++++++-- tests/test_op9_version_casting.py | 8 +- 11 files changed, 159 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 7d2fbaf..902eef4 100644 --- a/README.md +++ b/README.md @@ -321,7 +321,7 @@ A third-party vendor (ABC) registers a derived event type for order placement: "$id": "gts://gts.x.core.events.type.v1~abc.events.order_placed.v1~", // define a new event type derived from the base event type "type": "object", "allOf": [ - { "$ref": "gts.x.core.events.type.v1~" }, // inherit base event schema + { "$ref": "gts://gts.x.core.events.type.v1~" }, // inherit base event schema { "properties": { "typeId": { "const": "gts.x.core.events.type.v1~abc.orders.order_placed.v1~" }, @@ -639,7 +639,7 @@ This section demonstrates how different types of schema changes affect compatibi "$id": "gts://gts.x.core.events.type.v1~x.api.users.create_request.v1.0~", "type": "object", "allOf": [ - { "$ref": "gts.x.core.events.type.v1~" }, + { "$ref": "gts://gts.x.core.events.type.v1~" }, { "properties": { "payload": { @@ -663,7 +663,7 @@ This section demonstrates how different types of schema changes affect compatibi "$id": "gts://gts.x.core.events.type.v1~x.api.users.create_request.v1.1~", "type": "object", "allOf": [ - { "$ref": "gts.x.core.events.type.v1~" }, + { "$ref": "gts://gts.x.core.events.type.v1~" }, { "properties": { "payload": { @@ -706,7 +706,7 @@ This section demonstrates how different types of schema changes affect compatibi "$id": "gts://gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~", "type": "object", "allOf": [ - { "$ref": "gts.x.core.events.type.v1~" }, + { "$ref": "gts://gts.x.core.events.type.v1~" }, { "properties": { "payload": { @@ -731,7 +731,7 @@ This section demonstrates how different types of schema changes affect compatibi "$id": "gts://gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~", "type": "object", "allOf": [ - { "$ref": "gts.x.core.events.type.v1~" }, + { "$ref": "gts://gts.x.core.events.type.v1~" }, { "properties": { "payload": { @@ -775,7 +775,7 @@ This section demonstrates how different types of schema changes affect compatibi "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.core.events.type.v1~"}, + {"$$ref": "gts://gts.x.core.events.type.v1~"}, { "type": "object", "required": ["type", "payload"], @@ -799,7 +799,7 @@ This section demonstrates how different types of schema changes affect compatibi "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.core.events.type.v1~"}, + {"$$ref": "gts://gts.x.core.events.type.v1~"}, { "type": "object", "required": ["type", "payload"], @@ -935,7 +935,7 @@ Now, let's define the audit event schema for vendor `X` event manager: "title": "Audit Event, derived from Base Event", "type": "object", "allOf": [ - { "$ref": "gts.x.core.events.type.v1~" }, + { "$ref": "gts://gts.x.core.events.type.v1~" }, { "type": "object", "properties": { @@ -966,7 +966,7 @@ Then, let's define the schema of specific audit event registered by vendor `ABC` "title": "Vendor ABC Custom Purchase Audit Event from app APP", "type": "object", "allOf": [ - { "$ref": "gts.x.core.events.type.v1~x.core.audit.event.v1~" }, + { "$ref": "gts://gts.x.core.events.type.v1~x.core.audit.event.v1~" }, { "type": "object", "properties": { @@ -1143,6 +1143,20 @@ It is recommended to put the GTS **type identifier** into the JSON Schema `$id` Implementation note: GTS itself defines the canonical identifier string starting with `gts.`. When `$id` is expressed as `gts://...`, implementations should trim the `gts://` prefix and treat the remainder as the canonical GTS identifier for validation, comparison, and registry keys. The `gts://` prefix exists only to make `$id` URI-compatible. +**JSON Schema (`$ref`)** + +It is recommended to make GTS schema references in JSON Schema `$ref` URI-compatible the same way as `$id`, by prepending the `gts://` prefix when `$ref` points at a GTS schema identifier: + +```json +{ + "allOf": [ + { "$ref": "gts://gts.x.core.events.type.v1~" } + ] +} +``` + +Implementation note: When `$ref` is expressed as `gts://...`, implementations should trim the `gts://` prefix and treat the remainder as the canonical GTS identifier for resolution, validation, comparison, and registry keys. The `gts://` prefix exists only to make `$ref` URI-compatible. + **JSON instances (well-known vs anonymous)** - **Well-known instances (named)**: recommended to use a GTS identifier in the `id` field (alternatives: `gtsId`, `gts_id`). Prefer a chained identifier so the **left segment(s)** define the schema/type automatically, and the **rightmost** segment is the instance name. diff --git a/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~.schema.json b/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~.schema.json index e5d088c..012eb69 100644 --- a/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~.schema.json +++ b/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~.schema.json @@ -4,7 +4,7 @@ "title": "Event Instance Schema: order.placed", "type": "object", "allOf": [ - { "$ref": "gts.x.core.events.type.v1~" }, + { "$ref": "gts://gts.x.core.events.type.v1~" }, { "type": "object", "required": ["type", "payload", "subjectType"], diff --git a/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~.schema.json b/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~.schema.json index 309da48..33cbf71 100644 --- a/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~.schema.json +++ b/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~.schema.json @@ -4,7 +4,7 @@ "title": "Event Instance Schema: order.placed", "type": "object", "allOf": [ - { "$ref": "gts.x.core.events.type.v1~" }, + { "$ref": "gts://gts.x.core.events.type.v1~" }, { "type": "object", "required": ["type", "payload", "subjectType"], diff --git a/examples/events/schemas/gts.x.core.events.type.v1~x.core.idp.contact_created.v1~.schema.json b/examples/events/schemas/gts.x.core.events.type.v1~x.core.idp.contact_created.v1~.schema.json index 0337714..d6db51e 100644 --- a/examples/events/schemas/gts.x.core.events.type.v1~x.core.idp.contact_created.v1~.schema.json +++ b/examples/events/schemas/gts.x.core.events.type.v1~x.core.idp.contact_created.v1~.schema.json @@ -5,7 +5,7 @@ "description": "Event instance schema for the user.created event type", "type": "object", "allOf": [ - { "$ref": "gts.x.core.events.type.v1~" }, + { "$ref": "gts://gts.x.core.events.type.v1~" }, { "type": "object", "required": ["type", "payload", "subjectType"], diff --git a/examples/events/schemas/gts.x.core.idp.contact.v1.0~x.core.idp.billing_contact.v1.0~.schema.json b/examples/events/schemas/gts.x.core.idp.contact.v1.0~x.core.idp.billing_contact.v1.0~.schema.json index 4ba4533..189cbe8 100644 --- a/examples/events/schemas/gts.x.core.idp.contact.v1.0~x.core.idp.billing_contact.v1.0~.schema.json +++ b/examples/events/schemas/gts.x.core.idp.contact.v1.0~x.core.idp.billing_contact.v1.0~.schema.json @@ -4,7 +4,7 @@ "title": "User", "type": "object", "allOf": [ - { "$ref": "gts.x.core.idp.contact.v1.0~" }, + { "$ref": "gts://gts.x.core.idp.contact.v1.0~" }, { "type": "object", "properties": { diff --git a/examples/mcp/schemas/gts.x.ai.mcp.tool.v1~x.ai.mcp.http_outbound.v1~.schema.jsonc b/examples/mcp/schemas/gts.x.ai.mcp.tool.v1~x.ai.mcp.http_outbound.v1~.schema.jsonc index e338eab..76f0414 100644 --- a/examples/mcp/schemas/gts.x.ai.mcp.tool.v1~x.ai.mcp.http_outbound.v1~.schema.jsonc +++ b/examples/mcp/schemas/gts.x.ai.mcp.tool.v1~x.ai.mcp.http_outbound.v1~.schema.jsonc @@ -5,7 +5,7 @@ "title": "MCP Tool with HTTP Outbound Capability", "type": "object", "allOf": [ - { "$ref": "gts.x.ai.mcp.tool.v1~" }, + { "$ref": "gts://gts.x.ai.mcp.tool.v1~" }, { "type": "object", "properties": { diff --git a/tests/test_op10_query_execution.py b/tests/test_op10_query_execution.py index 4ba01e4..9cdfe49 100644 --- a/tests/test_op10_query_execution.py +++ b/tests/test_op10_query_execution.py @@ -525,7 +525,7 @@ def register_wildcard_usecase_entities(): "description": "System message derived from v1.0", "allOf": [ { - "$$ref": "gts.x.test10_llm.chat.message.v1.0~" + "$$ref": "gts://gts.x.test10_llm.chat.message.v1.0~" } ] }) @@ -557,7 +557,7 @@ def register_wildcard_usecase_entities(): "description": "User message derived from v1.1", "allOf": [ { - "$$ref": "gts.x.test10_llm.chat.message.v1.1~" + "$$ref": "gts://gts.x.test10_llm.chat.message.v1.1~" } ] }) diff --git a/tests/test_op11_attribute_access.py b/tests/test_op11_attribute_access.py index cc60326..c537ed1 100644 --- a/tests/test_op11_attribute_access.py +++ b/tests/test_op11_attribute_access.py @@ -45,7 +45,7 @@ def test_start(self): "$$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.test11.events.type.v1~"}, + {"$$ref": "gts://gts.x.test11.events.type.v1~"}, { "type": "object", "required": ["type", "payload"], @@ -260,7 +260,7 @@ def test_start(self): "$$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.test11.events.type.v1~"}, + {"$$ref": "gts://gts.x.test11.events.type.v1~"}, { "type": "object", "required": ["type", "payload"], diff --git a/tests/test_op6_schema_validation.py b/tests/test_op6_schema_validation.py index 4697258..e6d6561 100644 --- a/tests/test_op6_schema_validation.py +++ b/tests/test_op6_schema_validation.py @@ -40,7 +40,7 @@ def test_start(self): "$$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.test6.events.type.v1~"}, + {"$$ref": "gts://gts.x.test6.events.type.v1~"}, { "type": "object", "required": ["type", "payload"], @@ -138,7 +138,7 @@ def test_start(self): "$$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.test6.events.type.v1~"}, + {"$$ref": "gts://gts.x.test6.events.type.v1~"}, { "type": "object", "required": ["type", "payload"], diff --git a/tests/test_op7_relationship_resolution.py b/tests/test_op7_relationship_resolution.py index a506122..b01664a 100644 --- a/tests/test_op7_relationship_resolution.py +++ b/tests/test_op7_relationship_resolution.py @@ -38,11 +38,14 @@ def test_start(self): RunRequest("register derived event schema") .post("/entities") .with_json({ - "$$id": "gts://gts.x.test7.events.type.v1~x.test7.graph.event.v1.0~", + "$$id": ( + "gts://gts.x.test7.events.type.v1~" + "x.test7.graph.event.v1.0~" + ), "$$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.test7.events.type.v1~"}, + {"$$ref": "gts://gts.x.test7.events.type.v1~"}, { "type": "object", "required": ["type", "payload"], @@ -105,7 +108,7 @@ def test_start(self): "allOf": [ { "$$ref": ( - "gts.x.nonexistent.base.type.v1~" + "gts://gts.x.nonexistent.base.type.v1~" ) }, { @@ -130,6 +133,102 @@ def test_start(self): ] +class TestCaseTestOp7SchemaGraph_LocalRefAllowed(HttpRunner): + """OP#7 - Relationship Resolution: Allow local JSON Schema $ref""" + config = Config("OP#7 - Schema Graph (local $ref allowed)").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + Step( + RunRequest("register schema with local $ref should succeed") + .post("/entities") + .with_params(**{"validate": True}) + .with_json({ + "$$id": "gts://gts.x.test7.local_ref.allowed.v1~", + "$$schema": "http://json-schema.org/draft-07/schema#", + "$$defs": { + "Base": { + "type": "object", + "properties": {"id": {"type": "string"}}, + "required": ["id"], + "additionalProperties": False + } + }, + "allOf": [ + {"$$ref": "#/$$defs/Base"} + ] + }) + .validate() + .assert_equal("status_code", 200) + ), + ] + + +class TestCaseTestOp7SchemaGraph_RefNotGtsUri(HttpRunner): + """OP#7 - Reject non-GTS external $ref""" + config = Config("OP#7 - Schema Graph ($ref not gts://...)").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + Step( + RunRequest("register schema with non-GTS $ref should fail") + .post("/entities") + .with_params(**{"validate": True}) + .with_json({ + "$$id": "gts://gts.x.test7.invalid_ref.not_gts_uri.v1~", + "$$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "allOf": [ + { + "$$ref": "https://example.com/schemas/base.json" + } + ] + }) + .validate() + .assert_equal("status_code", 422) + ), + ] + + +class TestCaseTestOp7SchemaGraph_RefMalformedGts(HttpRunner): + """OP#7 - Reject malformed GTS $ref""" + config = Config("OP#7 - Schema Graph (malformed GTS in $ref)").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + Step( + RunRequest("register schema with malformed GTS $ref should fail") + .post("/entities") + .with_params(**{"validate": True}) + .with_json({ + "$$id": "gts://gts.x.test7.invalid_ref.malformed_gts.v1~", + "$$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "allOf": [ + { + # invalid segment token: hyphen is not allowed + "$$ref": "gts://gts.x.bad-seg.ns.type.v1~" + } + ] + }) + .validate() + .assert_equal("status_code", 422) + ), + ] + + class TestCaseTestOp7SchemaGraph_ComplexChain(HttpRunner): """OP#7 - Relationship Resolution: Multi-level inheritance chain""" config = Config("OP#7 - Schema Graph (complex chain)").base_url( @@ -161,11 +260,14 @@ def test_start(self): RunRequest("register level 1 derived schema") .post("/entities") .with_json({ - "$$id": "gts://gts.x.test7.base.type.v1~x.test7.derived1.type.v1~", + "$$id": ( + "gts://gts.x.test7.base.type.v1~" + "x.test7.derived1.type.v1~" + ), "$$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.test7.base.type.v1~"}, + {"$$ref": "gts://gts.x.test7.base.type.v1~"}, { "type": "object", "required": ["field1"], @@ -193,7 +295,7 @@ def test_start(self): "allOf": [ { "$$ref": ( - "gts.x.test7.base.type.v1~" + "gts://gts.x.test7.base.type.v1~" "x.test7.derived1.type.v1~" ) }, @@ -263,7 +365,7 @@ def test_start(self): "$$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.base.entity.root.v1~"}, + {"$$ref": "gts://gts.x.base.entity.root.v1~"}, { "properties": { "level": {"type": "integer", "const": 2} @@ -287,7 +389,12 @@ def test_start(self): "$$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.base.entity.root.v1~x.l2._.type.v1~"}, + { + "$$ref": ( + "gts://gts.x.base.entity.root.v1~" + "x.l2._.type.v1~" + ) + }, { "properties": { "level": {"type": "integer", "const": 3} @@ -314,7 +421,7 @@ def test_start(self): "allOf": [ { "$$ref": ( - "gts.x.base.entity.root.v1~" + "gts://gts.x.base.entity.root.v1~" "x.l2._.type.v1~" "x.l3._.type.v1~" ) @@ -346,7 +453,7 @@ def test_start(self): "allOf": [ { "$$ref": ( - "gts.x.base.entity.root.v1~" + "gts://gts.x.base.entity.root.v1~" "x.l2._.type.v1~" "x.l3._.type.v1~" "x.l4._.type.v1~" @@ -421,7 +528,7 @@ def test_start(self): "$$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.package_a.core.base.v1~"}, + {"$$ref": "gts://gts.x.package_a.core.base.v1~"}, { "properties": { "derivedField": {"type": "string"} @@ -447,7 +554,7 @@ def test_start(self): "allOf": [ { "$$ref": ( - "gts.x.package_a.core.base.v1~" + "gts://gts.x.package_a.core.base.v1~" "x.package_b._.derived.v1~" ) }, @@ -519,7 +626,7 @@ def test_start(self): "$$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.platform.events.base.v1~"}, + {"$$ref": "gts://gts.x.platform.events.base.v1~"}, { "properties": { "vendorData": {"type": "object"} @@ -545,7 +652,7 @@ def test_start(self): "allOf": [ { "$$ref": ( - "gts.x.platform.events.base.v1~" + "gts://gts.x.platform.events.base.v1~" "abc.app._.custom_event.v1~" ) }, diff --git a/tests/test_op9_version_casting.py b/tests/test_op9_version_casting.py index 018d231..0815b27 100644 --- a/tests/test_op9_version_casting.py +++ b/tests/test_op9_version_casting.py @@ -45,7 +45,7 @@ def test_start(self): "$$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.test9.events.type.v1~"}, + {"$$ref": "gts://gts.x.test9.events.type.v1~"}, { "type": "object", "required": ["type", "payload"], @@ -99,7 +99,7 @@ def test_start(self): "$$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.test9.events.type.v1~"}, + {"$$ref": "gts://gts.x.test9.events.type.v1~"}, { "type": "object", "required": ["type", "payload"], @@ -239,7 +239,7 @@ def test_start(self): "$$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.test9.events.type.v1~"}, + {"$$ref": "gts://gts.x.test9.events.type.v1~"}, { "type": "object", "required": ["type", "payload"], @@ -276,7 +276,7 @@ def test_start(self): "$$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "allOf": [ - {"$$ref": "gts.x.test9.events.type.v1~"}, + {"$$ref": "gts://gts.x.test9.events.type.v1~"}, { "type": "object", "required": ["type", "payload"], From d0387cec1155ad4083aef6330d7148d35e5d5f65 Mon Sep 17 00:00:00 2001 From: Artifizer Date: Mon, 22 Dec 2025 22:53:14 +0200 Subject: [PATCH 3/4] doc: update README.md to version 0.7 Signed-off-by: Artifizer --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 902eef4..b0b427a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -> **VERSION**: GTS specification draft, version 0.6 +> **VERSION**: GTS specification draft, version 0.7 # Global Type System (GTS) Specification @@ -90,6 +90,7 @@ See the [Practical Benefits for Service and Platform Vendors](#51-practical-bene | 0.4 | Clarify some corner cases - tokens must not start with digit, uuid5, minor version semantic | | 0.5 | Added Referece Implmenetation recommendations (section 9) | | 0.6 | Introduced well-known/anonymous instance term; defined field naming implementation recommendations | +| 0.7 | BREAKING: require $ref value to start with 'gts://'; strict rules for schema/instance distinction | ## 1. Motivation @@ -1155,6 +1156,8 @@ It is recommended to make GTS schema references in JSON Schema `$ref` URI-compat } ``` +Note: local JSON Schema references (e.g. `"$ref": "#/definitions/Foo"`, `"$ref": "#/$defs/Foo"`) are JSON Schema compliant and remain valid. The `gts://` recommendation applies only when `$ref` targets a GTS schema identifier. + Implementation note: When `$ref` is expressed as `gts://...`, implementations should trim the `gts://` prefix and treat the remainder as the canonical GTS identifier for resolution, validation, comparison, and registry keys. The `gts://` prefix exists only to make `$ref` URI-compatible. **JSON instances (well-known vs anonymous)** From 91351e795509d3ac7ea747d4c12797facf5b73b1 Mon Sep 17 00:00:00 2001 From: Artifizer Date: Mon, 22 Dec 2025 22:54:06 +0200 Subject: [PATCH 4/4] fix: update tests to reflect recent changes Signed-off-by: Artifizer --- tests/test_op7_relationship_resolution.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_op7_relationship_resolution.py b/tests/test_op7_relationship_resolution.py index b01664a..362fd09 100644 --- a/tests/test_op7_relationship_resolution.py +++ b/tests/test_op7_relationship_resolution.py @@ -135,7 +135,7 @@ def test_start(self): class TestCaseTestOp7SchemaGraph_LocalRefAllowed(HttpRunner): """OP#7 - Relationship Resolution: Allow local JSON Schema $ref""" - config = Config("OP#7 - Schema Graph (local $ref allowed)").base_url( + config = Config("OP#7 - Schema Graph (local $$ref allowed)").base_url( get_gts_base_url() ) @@ -146,7 +146,7 @@ def test_start(self): Step( RunRequest("register schema with local $ref should succeed") .post("/entities") - .with_params(**{"validate": True}) + .with_params(**{"validate": "true"}) .with_json({ "$$id": "gts://gts.x.test7.local_ref.allowed.v1~", "$$schema": "http://json-schema.org/draft-07/schema#", @@ -170,7 +170,7 @@ def test_start(self): class TestCaseTestOp7SchemaGraph_RefNotGtsUri(HttpRunner): """OP#7 - Reject non-GTS external $ref""" - config = Config("OP#7 - Schema Graph ($ref not gts://...)").base_url( + config = Config("OP#7 - Schema Graph ($$ref not gts://...)").base_url( get_gts_base_url() ) @@ -181,7 +181,7 @@ def test_start(self): Step( RunRequest("register schema with non-GTS $ref should fail") .post("/entities") - .with_params(**{"validate": True}) + .with_params(**{"validate": "true"}) .with_json({ "$$id": "gts://gts.x.test7.invalid_ref.not_gts_uri.v1~", "$$schema": "http://json-schema.org/draft-07/schema#", @@ -200,7 +200,7 @@ def test_start(self): class TestCaseTestOp7SchemaGraph_RefMalformedGts(HttpRunner): """OP#7 - Reject malformed GTS $ref""" - config = Config("OP#7 - Schema Graph (malformed GTS in $ref)").base_url( + config = Config("OP#7 - Schema Graph (malformed GTS in $$ref)").base_url( get_gts_base_url() ) @@ -211,7 +211,7 @@ def test_start(self): Step( RunRequest("register schema with malformed GTS $ref should fail") .post("/entities") - .with_params(**{"validate": True}) + .with_params(**{"validate": "true"}) .with_json({ "$$id": "gts://gts.x.test7.invalid_ref.malformed_gts.v1~", "$$schema": "http://json-schema.org/draft-07/schema#",