From 3e00914b440fb68669554218bfae264dbf54e507 Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:34:29 +0200 Subject: [PATCH] fix(openapi): make example de-duplication type-aware and order-insensitive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _merge_examples de-duped with `ex not in merged`, i.e. Python `==`. That collapses semantically distinct JSON examples: True == 1, False == 0, 1 == 1.0 — so mixing such values silently dropped one. Switch to a canonical JSON key (json.dumps with sorted keys) for identity: order-insensitive for objects and type-aware for scalars. Aligns the de-dup behavior with the TypeScript implementation. Bumps utcp-http 1.1.10 -> 1.1.11. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../http/pyproject.toml | 2 +- .../http/src/utcp_http/openapi_converter.py | 10 +++- .../http/tests/test_openapi_converter.py | 53 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/plugins/communication_protocols/http/pyproject.toml b/plugins/communication_protocols/http/pyproject.toml index e43174b..d6c7221 100644 --- a/plugins/communication_protocols/http/pyproject.toml +++ b/plugins/communication_protocols/http/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "utcp-http" -version = "1.1.10" +version = "1.1.11" authors = [ { name = "UTCP Contributors" }, ] diff --git a/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py b/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py index 26af4d6..7eb3504 100644 --- a/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py +++ b/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py @@ -401,13 +401,21 @@ def _merge_examples(self, *objs: Optional[Dict[str, Any]]) -> Optional[List[Any] Used to combine examples that can appear at more than one level for the same value, e.g. a Media Type Object and the Schema Object beneath it. Returns a list suitable for the JSON Schema 'examples' keyword, or None. + + De-duplication uses a canonical JSON serialization (sorted keys) as the + identity. This is order-insensitive for objects and type-aware, so it + does not collapse semantically distinct examples the way Python's ``==`` + would (``True == 1``, ``False == 0``, ``1 == 1.0``). """ merged: List[Any] = [] + seen: set = set() for obj in objs: if not isinstance(obj, dict): continue for ex in self._extract_examples(obj) or []: - if ex not in merged: + key = json.dumps(ex, sort_keys=True, default=str) + if key not in seen: + seen.add(key) merged.append(ex) return merged or None diff --git a/plugins/communication_protocols/http/tests/test_openapi_converter.py b/plugins/communication_protocols/http/tests/test_openapi_converter.py index d282b9d..30fdebf 100644 --- a/plugins/communication_protocols/http/tests/test_openapi_converter.py +++ b/plugins/communication_protocols/http/tests/test_openapi_converter.py @@ -370,3 +370,56 @@ def test_openapi_converter_array_form_schema_examples(): assert body_param.model_dump(by_alias=True).get("examples") == [{"name": "Gadget A"}, {"name": "Gadget B"}] assert tool.outputs.examples == ["ok", "done"] + + +def test_openapi_converter_example_dedup_is_type_aware_and_order_insensitive(): + """De-dup keeps distinct JSON types (true vs 1) and collapses key-reordered objects.""" + openapi_spec = { + "openapi": "3.0.0", + "info": {"title": "Test API", "version": "1.0.0"}, + "paths": { + "/items": { + "post": { + "operationId": "createItem", + "requestBody": { + "content": { + "application/json": { + # media-type example with one key order... + "examples": {"e1": {"value": {"a": 1, "b": 2}}}, + "schema": { + "type": "object", + # ...schema example with the other key order -> collapses to one + "examples": [{"b": 2, "a": 1}], + }, + } + } + }, + "responses": { + "200": { + "description": "ok", + "content": { + "application/json": { + "schema": { + "type": "object", + # true and 1 are distinct; duplicate true removed + "examples": [True, 1, True], + } + } + }, + } + }, + } + } + }, + } + + converter = OpenApiConverter(openapi_spec) + manual = converter.convert() + + tool = next((t for t in manual.tools if t.name == "createItem"), None) + assert tool is not None + + # {a:1,b:2} and {b:2,a:1} are the same example -> collapsed to one + assert tool.inputs.properties.get("body").examples == [{"a": 1, "b": 2}] + # True vs 1 kept distinct (== would have collapsed them); duplicate True removed + assert tool.outputs.examples == [True, 1]