From 6fe926249d0ee711f846c071be1821428ec1a4de Mon Sep 17 00:00:00 2001 From: XiaolongZhang Date: Thu, 30 Jul 2026 14:21:13 +0800 Subject: [PATCH 1/3] fix(preprocess): derive UcpMetadata union from $defs to include catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit normalize_metadata_schemas hardcoded the UcpMetadata root union as five members (platform, business, response_{checkout,order,cart}) and so silently omitted response_catalog_schema, which is defined in ucp.json at the 2026-04-08 release the SDK targets. The generated UcpMetadata (src/ucp_sdk/models/schemas/ucp.py) therefore lacked ResponseCatalogSchema (the class was generated from the $def, but the union omitted it), so a UCP message carrying catalog-response metadata failed validation against every model's `ucp` field. Derive the union members from ucp.json's $defs instead: the discovery profiles (platform/business) plus every response_*_schema. This fixes the missing catalog entry and keeps the union complete as the protocol adds response types, matching the data-driven approach used for minProperties (#55). Output order follows $defs insertion order, so existing members are unchanged and catalog is appended. Update the generated UcpMetadata in ucp.py accordingly. A full regeneration is intentionally not bundled here: the tracked generated tree predates the current ruff toolchain and regenerating adds unrelated formatting churn, so only the one union line is edited — exactly what regeneration produces for this fix. Add MetadataUnionTest covering the derivation (profiles + all response schemas, automatic pickup of new response types, exclusion of non-schema defs such as request_schema, empty $defs) and update the existing normalize_metadata_schemas test to assert the six-member union. --- preprocess_schemas.py | 28 +++++++---- src/ucp_sdk/models/schemas/ucp.py | 3 +- tests/test_codegen_pipeline.py | 78 ++++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 10 deletions(-) diff --git a/preprocess_schemas.py b/preprocess_schemas.py index 9294143..6346ccf 100644 --- a/preprocess_schemas.py +++ b/preprocess_schemas.py @@ -532,6 +532,25 @@ def generate_variants(path, schema, ops, global_variant_requirements): # --- Global Normalization --- +def metadata_union_members(ucp_schema): + """Return the ``$defs`` names that form the UcpMetadata root union. + + The union spans the discovery profiles (platform/business) and every + response schema declared in ``ucp.json``. Deriving the list from + ``$defs`` keeps the generated ``UcpMetadata`` complete as the protocol + adds response types — a previous hardcoded list silently omitted + ``response_catalog_schema``, dropping catalog responses from every + model's ``ucp`` field. + """ + defs = ucp_schema.get("$defs", {}) + return [ + name + for name in defs + if name in ("platform_schema", "business_schema") + or (name.startswith("response_") and name.endswith("_schema")) + ] + + def normalize_metadata_schemas(schemas, target_dir): """ Ensures ucp.json has a root union and other files point to it generically. @@ -542,14 +561,7 @@ def normalize_metadata_schemas(schemas, target_dir): if ucp_path in schemas: ucp = schemas[ucp_path] ucp["oneOf"] = [ - {"$ref": f"#/$defs/{d}"} - for d in [ - "platform_schema", - "business_schema", - "response_checkout_schema", - "response_order_schema", - "response_cart_schema", - ] + {"$ref": f"#/$defs/{name}"} for name in metadata_union_members(ucp) ] for p_abs, s in schemas.items(): diff --git a/src/ucp_sdk/models/schemas/ucp.py b/src/ucp_sdk/models/schemas/ucp.py index 4e8b977..373f7a9 100644 --- a/src/ucp_sdk/models/schemas/ucp.py +++ b/src/ucp_sdk/models/schemas/ucp.py @@ -338,7 +338,8 @@ class ResponseCatalogSchema(Base): | BusinessSchema | ResponseCheckoutSchema | ResponseOrderSchema - | ResponseCartSchema, + | ResponseCartSchema + | ResponseCatalogSchema, Field(..., title="UCP Metadata"), ], ) diff --git a/tests/test_codegen_pipeline.py b/tests/test_codegen_pipeline.py index 4890720..31f44cb 100644 --- a/tests/test_codegen_pipeline.py +++ b/tests/test_codegen_pipeline.py @@ -401,7 +401,18 @@ def test_normalize_metadata_schemas_sets_root_union_and_ucp_refs( (target_dir / "checkout_create_request.json").resolve() ) schemas = { - ucp_path: {"$defs": {}}, + ucp_path: { + "$defs": { + "version": {"type": "string"}, + "entity": {"type": "object"}, + "platform_schema": {"type": "object"}, + "business_schema": {"type": "object"}, + "response_checkout_schema": {"type": "object"}, + "response_order_schema": {"type": "object"}, + "response_cart_schema": {"type": "object"}, + "response_catalog_schema": {"type": "object"}, + } + }, checkout_path: { "properties": { "ucp": {"$ref": "ucp.json#/$defs/response_schema"} @@ -424,6 +435,7 @@ def test_normalize_metadata_schemas_sets_root_union_and_ucp_refs( {"$ref": "#/$defs/response_checkout_schema"}, {"$ref": "#/$defs/response_order_schema"}, {"$ref": "#/$defs/response_cart_schema"}, + {"$ref": "#/$defs/response_catalog_schema"}, ], ) self.assertEqual( @@ -546,6 +558,70 @@ def test_main_preprocesses_schema_tree_end_to_end(self) -> None: self.assertEqual(child_variant["required"], ["value"]) +class MetadataUnionTest(unittest.TestCase): + """The UcpMetadata root union is derived from ucp.json $defs.""" + + def test_includes_profiles_and_all_response_schemas(self) -> None: + """Profiles and every response_*_schema belong to the union.""" + ucp = { + "$defs": { + "version": {"type": "string"}, + "version_constraint": {"type": "object"}, + "requires": {"type": "object"}, + "entity": {"type": "object"}, + "base": {"type": "object"}, + "success": {"type": "object"}, + "error": {"type": "object"}, + "platform_schema": {"type": "object"}, + "business_schema": {"type": "object"}, + "response_checkout_schema": {"type": "object"}, + "response_order_schema": {"type": "object"}, + "response_cart_schema": {"type": "object"}, + "response_catalog_schema": {"type": "object"}, + } + } + self.assertEqual( + preprocess_schemas.metadata_union_members(ucp), + [ + "platform_schema", + "business_schema", + "response_checkout_schema", + "response_order_schema", + "response_cart_schema", + "response_catalog_schema", + ], + ) + + def test_picks_up_new_response_types_automatically(self) -> None: + """A response schema added upstream is included without code changes.""" + ucp = { + "$defs": { + "platform_schema": {"type": "object"}, + "business_schema": {"type": "object"}, + "response_invoice_schema": {"type": "object"}, + } + } + self.assertEqual( + preprocess_schemas.metadata_union_members(ucp), + ["platform_schema", "business_schema", "response_invoice_schema"], + ) + + def test_excludes_non_schema_defs(self) -> None: + """Helper and shared defs never leak into the metadata union.""" + ucp = { + "$defs": { + "entity": {"type": "object"}, + "request_schema": {"type": "object"}, + "base": {"type": "object"}, + } + } + self.assertEqual(preprocess_schemas.metadata_union_members(ucp), []) + + def test_empty_defs_yields_empty_union(self) -> None: + """No $defs means no union members.""" + self.assertEqual(preprocess_schemas.metadata_union_members({}), []) + + @unittest.skipUnless( HAVE_SDK, "requires the installed package (pip install -e .)" ) From c59be303d4e3c4fce2ad549d2df133ec0bcd518f Mon Sep 17 00:00:00 2001 From: damaz91 Date: Fri, 31 Jul 2026 12:35:38 +0000 Subject: [PATCH 2/3] fix(models): include catalog response in metadata request variants --- src/ucp_sdk/models/schemas/__init__.py | 1 + src/ucp_sdk/models/schemas/common/__init__.py | 1 + src/ucp_sdk/models/schemas/shopping/__init__.py | 1 + src/ucp_sdk/models/schemas/shopping/types/__init__.py | 1 + src/ucp_sdk/models/schemas/transports/__init__.py | 1 + src/ucp_sdk/models/schemas/ucp_create_request.py | 3 ++- src/ucp_sdk/models/schemas/ucp_update_request.py | 3 ++- 7 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ucp_sdk/models/schemas/__init__.py b/src/ucp_sdk/models/schemas/__init__.py index 1252d6b..421dc21 100644 --- a/src/ucp_sdk/models/schemas/__init__.py +++ b/src/ucp_sdk/models/schemas/__init__.py @@ -15,3 +15,4 @@ # generated by datamodel-codegen # pylint: disable=all # pyformat: disable + diff --git a/src/ucp_sdk/models/schemas/common/__init__.py b/src/ucp_sdk/models/schemas/common/__init__.py index 1252d6b..421dc21 100644 --- a/src/ucp_sdk/models/schemas/common/__init__.py +++ b/src/ucp_sdk/models/schemas/common/__init__.py @@ -15,3 +15,4 @@ # generated by datamodel-codegen # pylint: disable=all # pyformat: disable + diff --git a/src/ucp_sdk/models/schemas/shopping/__init__.py b/src/ucp_sdk/models/schemas/shopping/__init__.py index 1252d6b..421dc21 100644 --- a/src/ucp_sdk/models/schemas/shopping/__init__.py +++ b/src/ucp_sdk/models/schemas/shopping/__init__.py @@ -15,3 +15,4 @@ # generated by datamodel-codegen # pylint: disable=all # pyformat: disable + diff --git a/src/ucp_sdk/models/schemas/shopping/types/__init__.py b/src/ucp_sdk/models/schemas/shopping/types/__init__.py index 1252d6b..421dc21 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/__init__.py +++ b/src/ucp_sdk/models/schemas/shopping/types/__init__.py @@ -15,3 +15,4 @@ # generated by datamodel-codegen # pylint: disable=all # pyformat: disable + diff --git a/src/ucp_sdk/models/schemas/transports/__init__.py b/src/ucp_sdk/models/schemas/transports/__init__.py index 1252d6b..421dc21 100644 --- a/src/ucp_sdk/models/schemas/transports/__init__.py +++ b/src/ucp_sdk/models/schemas/transports/__init__.py @@ -15,3 +15,4 @@ # generated by datamodel-codegen # pylint: disable=all # pyformat: disable + diff --git a/src/ucp_sdk/models/schemas/ucp_create_request.py b/src/ucp_sdk/models/schemas/ucp_create_request.py index 5f68cf8..830d878 100644 --- a/src/ucp_sdk/models/schemas/ucp_create_request.py +++ b/src/ucp_sdk/models/schemas/ucp_create_request.py @@ -338,7 +338,8 @@ class ResponseCatalogSchema(Base): | BusinessSchema | ResponseCheckoutSchema | ResponseOrderSchema - | ResponseCartSchema, + | ResponseCartSchema + | ResponseCatalogSchema, Field(..., title="UCP Metadata Create Request"), ], ) diff --git a/src/ucp_sdk/models/schemas/ucp_update_request.py b/src/ucp_sdk/models/schemas/ucp_update_request.py index 57cf54c..423f664 100644 --- a/src/ucp_sdk/models/schemas/ucp_update_request.py +++ b/src/ucp_sdk/models/schemas/ucp_update_request.py @@ -338,7 +338,8 @@ class ResponseCatalogSchema(Base): | BusinessSchema | ResponseCheckoutSchema | ResponseOrderSchema - | ResponseCartSchema, + | ResponseCartSchema + | ResponseCatalogSchema, Field(..., title="UCP Metadata Update Request"), ], ) From b09f5e6494668870946ed40621ab364b4e37d3d2 Mon Sep 17 00:00:00 2001 From: damaz91 Date: Fri, 31 Jul 2026 14:08:24 +0000 Subject: [PATCH 3/3] style: fix end of file newlines in regenerated schema modules --- src/ucp_sdk/models/schemas/__init__.py | 1 - src/ucp_sdk/models/schemas/common/__init__.py | 1 - src/ucp_sdk/models/schemas/shopping/__init__.py | 1 - src/ucp_sdk/models/schemas/shopping/types/__init__.py | 1 - src/ucp_sdk/models/schemas/transports/__init__.py | 1 - 5 files changed, 5 deletions(-) diff --git a/src/ucp_sdk/models/schemas/__init__.py b/src/ucp_sdk/models/schemas/__init__.py index 421dc21..1252d6b 100644 --- a/src/ucp_sdk/models/schemas/__init__.py +++ b/src/ucp_sdk/models/schemas/__init__.py @@ -15,4 +15,3 @@ # generated by datamodel-codegen # pylint: disable=all # pyformat: disable - diff --git a/src/ucp_sdk/models/schemas/common/__init__.py b/src/ucp_sdk/models/schemas/common/__init__.py index 421dc21..1252d6b 100644 --- a/src/ucp_sdk/models/schemas/common/__init__.py +++ b/src/ucp_sdk/models/schemas/common/__init__.py @@ -15,4 +15,3 @@ # generated by datamodel-codegen # pylint: disable=all # pyformat: disable - diff --git a/src/ucp_sdk/models/schemas/shopping/__init__.py b/src/ucp_sdk/models/schemas/shopping/__init__.py index 421dc21..1252d6b 100644 --- a/src/ucp_sdk/models/schemas/shopping/__init__.py +++ b/src/ucp_sdk/models/schemas/shopping/__init__.py @@ -15,4 +15,3 @@ # generated by datamodel-codegen # pylint: disable=all # pyformat: disable - diff --git a/src/ucp_sdk/models/schemas/shopping/types/__init__.py b/src/ucp_sdk/models/schemas/shopping/types/__init__.py index 421dc21..1252d6b 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/__init__.py +++ b/src/ucp_sdk/models/schemas/shopping/types/__init__.py @@ -15,4 +15,3 @@ # generated by datamodel-codegen # pylint: disable=all # pyformat: disable - diff --git a/src/ucp_sdk/models/schemas/transports/__init__.py b/src/ucp_sdk/models/schemas/transports/__init__.py index 421dc21..1252d6b 100644 --- a/src/ucp_sdk/models/schemas/transports/__init__.py +++ b/src/ucp_sdk/models/schemas/transports/__init__.py @@ -15,4 +15,3 @@ # generated by datamodel-codegen # pylint: disable=all # pyformat: disable -