Skip to content

Commit 8b32145

Browse files
h3xxitclaude
andcommitted
fix(openapi): preserve array-form (JSON Schema / OAS 3.1) examples
_extract_examples only handled 'examples' as an OpenAPI map of named Example Objects, while _schema_without_example_keys strips 'examples' from schemas before spreading. So a JSON Schema / OpenAPI 3.1 schema-level 'examples' list was extracted as nothing and then dropped - silent data loss. Handle the list form too: list entries are literal example values; the map form keeps its Example Object 'value' handling. Adds a test covering array-form schema examples on both request body and response. Bumps utcp-http 1.1.9 -> 1.1.10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bb1ee06 commit 8b32145

3 files changed

Lines changed: 73 additions & 9 deletions

File tree

plugins/communication_protocols/http/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "utcp-http"
7-
version = "1.1.9"
7+
version = "1.1.10"
88
authors = [
99
{ name = "UTCP Contributors" },
1010
]

plugins/communication_protocols/http/src/utcp_http/openapi_converter.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -360,20 +360,30 @@ def _resolve_ref_obj(self, obj: Any, visited: Optional[set] = None) -> Any:
360360

361361
def _extract_examples(self, obj: Dict[str, Any]) -> Optional[List[Any]]:
362362
"""
363-
Extract examples from an OpenAPI parameter or Media Type Object (Parameter, Media Type, Schema).
364-
365-
Supports both 'example' (single value) and 'examples' (map of Example Objects).
363+
Extract examples from an OpenAPI Parameter, Media Type, or Schema object.
364+
365+
Handles all three shapes the spec allows:
366+
- 'example' (single value) - OpenAPI Parameter / Media Type / 3.0 Schema.
367+
- 'examples' as a map of named Example Objects - OpenAPI Parameter /
368+
Media Type Object (each entry carries an inline 'value').
369+
- 'examples' as a list of literal values - JSON Schema / OpenAPI 3.1
370+
Schema Object.
371+
366372
Returns a list of example values suitable for JSON Schema 'examples' keyword.
367373
"""
368374
examples = []
369-
375+
370376
# Handle single 'example' field
371377
if "example" in obj and obj["example"] is not None:
372378
examples.append(obj["example"])
373-
374-
# Handle 'examples' map (OpenAPI 3.0+)
375-
if "examples" in obj and isinstance(obj["examples"], dict):
376-
for example_obj in obj["examples"].values():
379+
380+
examples_obj = obj.get("examples")
381+
if isinstance(examples_obj, list):
382+
# JSON Schema / OpenAPI 3.1 Schema form: a plain list of example values.
383+
examples.extend(examples_obj)
384+
elif isinstance(examples_obj, dict):
385+
# OpenAPI 3.0 form: a map of named Example Objects.
386+
for example_obj in examples_obj.values():
377387
if isinstance(example_obj, dict) and "$ref" in example_obj:
378388
example_obj = self._resolve_ref_obj(example_obj, set()) or {}
379389
if isinstance(example_obj, dict):

plugins/communication_protocols/http/tests/test_openapi_converter.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,57 @@ def test_openapi_converter_schema_level_examples_normalized():
316316
assert "example" not in body_param.model_dump(by_alias=True)
317317

318318
assert tool.outputs.examples == [{"id": "w_1"}]
319+
320+
321+
def test_openapi_converter_array_form_schema_examples():
322+
"""Array-form (JSON Schema / OpenAPI 3.1) schema 'examples' are preserved, not dropped."""
323+
openapi_spec = {
324+
"openapi": "3.1.0",
325+
"info": {"title": "Test API", "version": "1.0.0"},
326+
"paths": {
327+
"/gadgets": {
328+
"post": {
329+
"operationId": "createGadget",
330+
"requestBody": {
331+
"content": {
332+
"application/json": {
333+
"schema": {
334+
"type": "object",
335+
"properties": {"name": {"type": "string"}},
336+
# JSON Schema 'examples' keyword: a list of values
337+
"examples": [{"name": "Gadget A"}, {"name": "Gadget B"}],
338+
}
339+
}
340+
}
341+
},
342+
"responses": {
343+
"200": {
344+
"description": "ok",
345+
"content": {
346+
"application/json": {
347+
"schema": {
348+
"type": "string",
349+
"examples": ["ok", "done"],
350+
}
351+
}
352+
},
353+
}
354+
},
355+
}
356+
}
357+
},
358+
}
359+
360+
converter = OpenApiConverter(openapi_spec)
361+
manual = converter.convert()
362+
363+
tool = next((t for t in manual.tools if t.name == "createGadget"), None)
364+
assert tool is not None
365+
366+
body_param = tool.inputs.properties.get("body")
367+
assert body_param is not None
368+
assert body_param.examples == [{"name": "Gadget A"}, {"name": "Gadget B"}]
369+
# examples surface in the normalized field on serialization
370+
assert body_param.model_dump(by_alias=True).get("examples") == [{"name": "Gadget A"}, {"name": "Gadget B"}]
371+
372+
assert tool.outputs.examples == ["ok", "done"]

0 commit comments

Comments
 (0)