Skip to content

Commit 16d708e

Browse files
Python: added operationid validation in openapi spec (#12551)
Description This PR enforces the presence of an explicit `operationId` for every operation in the OpenAPI specification. Previously, if `operationId` was missing, a default value was generated using the path and HTTP method. Now, the parser will raise a PluginInitializationError if operationId is not provided, ensuring that all operations are uniquely and explicitly identified as required by the OpenAPI standard. Key changes: - Added validation to check for the existence of operationId in each operation. - If operationId is missing, a clear error is raised, preventing fallback to a default value. - [python/tests/unit/connectors/openapi_plugin/no-operationid-openapi.yaml](https://github.com/microsoft/semantic-kernel/compare/main...sshandilya1991:semantic-kernel:sshandilya/12443_add_operation_id_validation?expand=1#diff-3d272039b3afa97f89805f0ee0258a98b613f0f373c603467b336a8912d5651a) serves as a test case. - Closes #12443 Edit: - Added more validation for operationId to be unique & throw in case duplicate operationId found while parsing.
1 parent 2bfa31b commit 16d708e

File tree

4 files changed

+70
-1
lines changed

4 files changed

+70
-1
lines changed

python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ def create_rest_api_operations(
206206

207207
paths = parsed_document.get("paths", {})
208208
request_objects = {}
209+
unique_operation_ids_registered: set[str] = set()
209210

210211
servers = parsed_document.get("servers", [])
211212

@@ -230,7 +231,15 @@ def create_rest_api_operations(
230231
for path, methods in paths.items():
231232
for method, details in methods.items():
232233
request_method = method.lower()
233-
operationId = details.get("operationId", path + "_" + request_method)
234+
# Validate that operationId exists
235+
if "operationId" not in details:
236+
raise PluginInitializationError(f"operationId missing, path: '{path}', method: '{method}'")
237+
operationId = details["operationId"]
238+
if operationId in unique_operation_ids_registered:
239+
raise PluginInitializationError(
240+
f"Duplicate operationId: '{operationId}', path: '{path}', method: '{method}'"
241+
)
242+
unique_operation_ids_registered.add(operationId)
234243

235244
summary = details.get("summary", None)
236245
description = details.get("description", None)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Simple HTTPBin API
4+
version: 1.0.0
5+
6+
servers:
7+
- url: https://httpbin.org
8+
9+
paths:
10+
/get:
11+
get:
12+
operationId: duplicateId
13+
summary: Simple GET request to httpbin.org
14+
responses:
15+
'200':
16+
description: Successful response from httpbin
17+
content:
18+
application/json:
19+
schema:
20+
type: object
21+
22+
/ip:
23+
get:
24+
operationId: duplicateId
25+
summary: Get client IP address from httpbin
26+
responses:
27+
'200':
28+
description: Successful response with IP
29+
content:
30+
application/json:
31+
schema:
32+
type: object
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Simple HTTPBin API
4+
version: 1.0.0
5+
servers:
6+
- url: https://httpbin.org
7+
paths:
8+
/get:
9+
get:
10+
summary: Simple GET request to httpbin.org
11+
responses:
12+
'200':
13+
description: Successful response from httpbin
14+
content:
15+
application/json:
16+
schema:
17+
type: object

python/tests/unit/connectors/openapi_plugin/test_openapi_parser.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,14 @@ def test_it_adds_security_metadata_to_operation(document_file_name, security_typ
178178
if found:
179179
break
180180
assert found, f"Security type '{security_type}' not found in operation '{operation.id}'"
181+
182+
183+
def test_no_operationid_raises_error():
184+
"""Test that OpenAPI spec without operationId raises PluginInitializationError."""
185+
no_op_path = os.path.join(current_dir, "no-operationid-openapi.yaml")
186+
with pytest.raises(PluginInitializationError, match="operationId missing"):
187+
create_functions_from_openapi(
188+
plugin_name="noOpTest",
189+
openapi_document_path=no_op_path,
190+
execution_settings=None,
191+
)

0 commit comments

Comments
 (0)