From 4cf6793751590bcb664665eae71770ace5100fd2 Mon Sep 17 00:00:00 2001
From: xujiantop-crypto <265865031+xujiantop-crypto@users.noreply.github.com>
Date: Mon, 26 Oct 2026 14:31:34 +0800
Subject: [PATCH] fix(openapi): preserve Swagger response examples
---
.../http/src/utcp_http/openapi_converter.py | 5 +
.../tests/test_openapi_response_examples.py | 109 ++++++++++++++++++
2 files changed, 114 insertions(+)
create mode 100644 plugins/communication_protocols/http/tests/test_openapi_response_examples.py
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 c333a27..426a348 100644
--- a/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py
+++ b/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py
@@ -775,6 +775,11 @@ def _extract_outputs(self, operation: Dict[str, Any]) -> JsonSchema:
break
elif "schema" in success_response: # OpenAPI 2.0
json_schema = success_response.get("schema")
+ # Swagger response examples map MIME types to raw values, not
+ # OpenAPI 3 Example Objects. Normalize before merging with schema examples.
+ examples = success_response.get("examples")
+ if isinstance(examples, dict):
+ media_type_obj = {"examples": list(examples.values())}
if not json_schema:
return JsonSchema()
diff --git a/plugins/communication_protocols/http/tests/test_openapi_response_examples.py b/plugins/communication_protocols/http/tests/test_openapi_response_examples.py
new file mode 100644
index 0000000..211f010
--- /dev/null
+++ b/plugins/communication_protocols/http/tests/test_openapi_response_examples.py
@@ -0,0 +1,109 @@
+from copy import deepcopy
+
+import pytest
+
+from utcp_http.openapi_converter import OpenApiConverter
+
+
+def swagger_spec(response, status="200"):
+ return {
+ "swagger": "2.0",
+ "info": {"title": "Example API", "version": "1.0"},
+ "host": "api.example.com",
+ "schemes": ["https"],
+ "paths": {
+ "/items": {
+ "get": {
+ "operationId": "getItems",
+ "responses": {status: response},
+ }
+ }
+ },
+ }
+
+
+@pytest.mark.parametrize(
+ "schema, example",
+ [
+ ({"type": "object"}, {"value": "literal", "summary": "payload"}),
+ ({"type": "object"}, {"$ref": "#/definitions/Result"}),
+ ({"type": "object"}, {"externalValue": "https://example.com/sample"}),
+ ({"type": "array", "items": {"type": "integer"}}, [1, 2]),
+ ({"type": "array", "items": {"type": "integer"}}, []),
+ ({"type": "boolean"}, False),
+ ({"type": "integer"}, 0),
+ ({"type": "string"}, ""),
+ ({"type": "object", "x-nullable": True}, None),
+ ],
+)
+def test_swagger_response_examples_preserve_raw_values(schema, example):
+ spec = swagger_spec({
+ "description": "OK",
+ "schema": schema,
+ "examples": {"application/json": example},
+ })
+ spec["definitions"] = {"Result": {"type": "string", "example": "resolved"}}
+ original = deepcopy(spec)
+
+ tool = OpenApiConverter(spec).convert().tools[0]
+
+ assert tool.outputs.examples == [example]
+ assert tool.outputs.model_dump(by_alias=True)["examples"] == [example]
+ assert spec == original
+
+
+@pytest.mark.parametrize("status", ["200", "201", "default"])
+def test_swagger_response_examples_merge_media_types_and_schema(status):
+ spec = swagger_spec({
+ "description": "OK",
+ "schema": {"$ref": "#/definitions/Result"},
+ "examples": {
+ "application/json": {"id": 1, "name": "widget"},
+ "application/vnd.example+json": {"name": "widget", "id": 1},
+ "application/xml": "- widget
",
+ },
+ }, status=status)
+ spec["definitions"] = {
+ "Result": {
+ "type": "object",
+ "properties": {"id": {"type": "integer"}, "name": {"type": "string"}},
+ "example": {"id": 2, "name": "other"},
+ }
+ }
+
+ tool = OpenApiConverter(spec).convert().tools[0]
+
+ assert tool.outputs.properties["id"].type == "integer"
+ assert tool.outputs.examples == [
+ {"id": 1, "name": "widget"},
+ "- widget
",
+ {"id": 2, "name": "other"},
+ ]
+
+
+def test_swagger_response_examples_deduplicate_without_collapsing_json_types():
+ spec = swagger_spec({
+ "description": "OK",
+ "schema": {"description": "A JSON value", "example": True},
+ "examples": {
+ "application/json": True,
+ "application/vnd.example+json": 1,
+ "application/vnd.other+json": True,
+ },
+ })
+
+ examples = OpenApiConverter(spec).convert().tools[0].outputs.examples
+
+ assert examples == [True, 1]
+ assert [type(value) for value in examples] == [bool, int]
+
+
+@pytest.mark.parametrize("response_examples", [{}, None, ["not a MIME map"]])
+def test_swagger_response_without_example_map_keeps_schema_example(response_examples):
+ spec = swagger_spec({
+ "description": "OK",
+ "schema": {"type": "string", "example": "schema example"},
+ "examples": response_examples,
+ })
+
+ assert OpenApiConverter(spec).convert().tools[0].outputs.examples == ["schema example"]