Spaces:
Running
Running
Jeremiah Lowin Claude commited on
Fix nullable field handling in OpenAPI to JSON Schema conversion (#1279)
Browse files
src/fastmcp/experimental/utilities/openapi/schemas.py
CHANGED
|
@@ -199,6 +199,86 @@ def _make_optional_parameter_nullable(schema: dict[str, Any]) -> dict[str, Any]:
|
|
| 199 |
return schema
|
| 200 |
|
| 201 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
def _combine_schemas_and_map_params(
|
| 203 |
route: HTTPRoute,
|
| 204 |
) -> tuple[dict[str, Any], dict[str, dict[str, str]]]:
|
|
@@ -499,6 +579,19 @@ def extract_output_schema_from_responses(
|
|
| 499 |
# Clean and copy the schema
|
| 500 |
output_schema = schema.copy()
|
| 501 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
# MCP requires output schemas to be objects. If this schema is not an object,
|
| 503 |
# we need to wrap it similar to how ParsedFunction.from_function() does it
|
| 504 |
if output_schema.get("type") != "object":
|
|
@@ -511,9 +604,13 @@ def extract_output_schema_from_responses(
|
|
| 511 |
}
|
| 512 |
output_schema = wrapped_schema
|
| 513 |
|
| 514 |
-
# Add schema definitions if available
|
| 515 |
-
if
|
| 516 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
|
| 518 |
# Use lightweight compression - prune additionalProperties and unused definitions
|
| 519 |
if output_schema.get("additionalProperties") is False:
|
|
@@ -564,5 +661,6 @@ __all__ = [
|
|
| 564 |
"extract_output_schema_from_responses",
|
| 565 |
"_replace_ref_with_defs",
|
| 566 |
"_make_optional_parameter_nullable",
|
|
|
|
| 567 |
"_adjust_union_types",
|
| 568 |
]
|
|
|
|
| 199 |
return schema
|
| 200 |
|
| 201 |
|
| 202 |
+
def _add_null_to_type(schema: dict[str, Any]) -> None:
|
| 203 |
+
"""Add 'null' to the schema's type field or handle oneOf/anyOf/allOf constructs if not already present."""
|
| 204 |
+
if "type" in schema:
|
| 205 |
+
current_type = schema["type"]
|
| 206 |
+
|
| 207 |
+
if isinstance(current_type, str):
|
| 208 |
+
# Convert string type to array with null
|
| 209 |
+
schema["type"] = [current_type, "null"]
|
| 210 |
+
elif isinstance(current_type, list):
|
| 211 |
+
# Add null to array if not already present
|
| 212 |
+
if "null" not in current_type:
|
| 213 |
+
schema["type"] = current_type + ["null"]
|
| 214 |
+
elif "oneOf" in schema:
|
| 215 |
+
# Convert oneOf to anyOf with null type
|
| 216 |
+
schema["anyOf"] = schema.pop("oneOf") + [{"type": "null"}]
|
| 217 |
+
elif "anyOf" in schema:
|
| 218 |
+
# Add null type to anyOf if not already present
|
| 219 |
+
if not any(item.get("type") == "null" for item in schema["anyOf"]):
|
| 220 |
+
schema["anyOf"].append({"type": "null"})
|
| 221 |
+
elif "allOf" in schema:
|
| 222 |
+
# For allOf, wrap in anyOf with null - this means (all conditions) OR null
|
| 223 |
+
schema["anyOf"] = [{"allOf": schema.pop("allOf")}, {"type": "null"}]
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def _handle_nullable_fields(schema: dict[str, Any] | Any) -> dict[str, Any] | Any:
|
| 227 |
+
"""Convert OpenAPI nullable fields to JSON Schema format: {"type": "string",
|
| 228 |
+
"nullable": true} -> {"type": ["string", "null"]}"""
|
| 229 |
+
|
| 230 |
+
if not isinstance(schema, dict):
|
| 231 |
+
return schema
|
| 232 |
+
|
| 233 |
+
# Check if we need to modify anything first to avoid unnecessary copying
|
| 234 |
+
has_root_nullable_field = "nullable" in schema
|
| 235 |
+
has_root_nullable_true = (
|
| 236 |
+
has_root_nullable_field
|
| 237 |
+
and schema["nullable"]
|
| 238 |
+
and (
|
| 239 |
+
"type" in schema
|
| 240 |
+
or "oneOf" in schema
|
| 241 |
+
or "anyOf" in schema
|
| 242 |
+
or "allOf" in schema
|
| 243 |
+
)
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
has_property_nullable_field = False
|
| 247 |
+
if "properties" in schema:
|
| 248 |
+
for prop_schema in schema["properties"].values():
|
| 249 |
+
if isinstance(prop_schema, dict) and "nullable" in prop_schema:
|
| 250 |
+
has_property_nullable_field = True
|
| 251 |
+
break
|
| 252 |
+
|
| 253 |
+
# If no nullable fields at all, return original schema unchanged
|
| 254 |
+
if not has_root_nullable_field and not has_property_nullable_field:
|
| 255 |
+
return schema
|
| 256 |
+
|
| 257 |
+
# Only copy if we need to modify
|
| 258 |
+
result = schema.copy()
|
| 259 |
+
|
| 260 |
+
# Handle root level nullable - always remove the field, convert type if true
|
| 261 |
+
if has_root_nullable_field:
|
| 262 |
+
result.pop("nullable")
|
| 263 |
+
if has_root_nullable_true:
|
| 264 |
+
_add_null_to_type(result)
|
| 265 |
+
|
| 266 |
+
# Handle properties nullable fields
|
| 267 |
+
if has_property_nullable_field and "properties" in result:
|
| 268 |
+
for prop_name, prop_schema in result["properties"].items():
|
| 269 |
+
if isinstance(prop_schema, dict) and "nullable" in prop_schema:
|
| 270 |
+
nullable_value = prop_schema.pop("nullable")
|
| 271 |
+
if nullable_value and (
|
| 272 |
+
"type" in prop_schema
|
| 273 |
+
or "oneOf" in prop_schema
|
| 274 |
+
or "anyOf" in prop_schema
|
| 275 |
+
or "allOf" in prop_schema
|
| 276 |
+
):
|
| 277 |
+
_add_null_to_type(prop_schema)
|
| 278 |
+
|
| 279 |
+
return result
|
| 280 |
+
|
| 281 |
+
|
| 282 |
def _combine_schemas_and_map_params(
|
| 283 |
route: HTTPRoute,
|
| 284 |
) -> tuple[dict[str, Any], dict[str, dict[str, str]]]:
|
|
|
|
| 579 |
# Clean and copy the schema
|
| 580 |
output_schema = schema.copy()
|
| 581 |
|
| 582 |
+
# If schema has a $ref, resolve it first before processing nullable fields
|
| 583 |
+
if "$ref" in output_schema and schema_definitions:
|
| 584 |
+
ref_path = output_schema["$ref"]
|
| 585 |
+
if ref_path.startswith("#/components/schemas/"):
|
| 586 |
+
schema_name = ref_path.split("/")[-1]
|
| 587 |
+
if schema_name in schema_definitions:
|
| 588 |
+
# Replace $ref with the actual schema definition
|
| 589 |
+
output_schema = schema_definitions[schema_name].copy()
|
| 590 |
+
|
| 591 |
+
# Handle OpenAPI nullable fields by converting them to JSON Schema format
|
| 592 |
+
# This prevents "None is not of type 'string'" validation errors
|
| 593 |
+
output_schema = _handle_nullable_fields(output_schema)
|
| 594 |
+
|
| 595 |
# MCP requires output schemas to be objects. If this schema is not an object,
|
| 596 |
# we need to wrap it similar to how ParsedFunction.from_function() does it
|
| 597 |
if output_schema.get("type") != "object":
|
|
|
|
| 604 |
}
|
| 605 |
output_schema = wrapped_schema
|
| 606 |
|
| 607 |
+
# Add schema definitions if available and handle nullable fields in them
|
| 608 |
+
# Only add $defs if we didn't resolve the $ref inline above
|
| 609 |
+
if schema_definitions and "$ref" not in schema.copy():
|
| 610 |
+
processed_defs = {}
|
| 611 |
+
for def_name, def_schema in schema_definitions.items():
|
| 612 |
+
processed_defs[def_name] = _handle_nullable_fields(def_schema)
|
| 613 |
+
output_schema["$defs"] = processed_defs
|
| 614 |
|
| 615 |
# Use lightweight compression - prune additionalProperties and unused definitions
|
| 616 |
if output_schema.get("additionalProperties") is False:
|
|
|
|
| 661 |
"extract_output_schema_from_responses",
|
| 662 |
"_replace_ref_with_defs",
|
| 663 |
"_make_optional_parameter_nullable",
|
| 664 |
+
"_handle_nullable_fields",
|
| 665 |
"_adjust_union_types",
|
| 666 |
]
|
src/fastmcp/utilities/openapi.py
CHANGED
|
@@ -187,6 +187,7 @@ __all__ = [
|
|
| 187 |
"parse_openapi_to_http_routes",
|
| 188 |
"extract_output_schema_from_responses",
|
| 189 |
"format_deep_object_parameter",
|
|
|
|
| 190 |
]
|
| 191 |
|
| 192 |
# Type variables for generic parser
|
|
@@ -1169,6 +1170,86 @@ def _make_optional_parameter_nullable(schema: dict[str, Any]) -> dict[str, Any]:
|
|
| 1169 |
return schema
|
| 1170 |
|
| 1171 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1172 |
def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
|
| 1173 |
"""
|
| 1174 |
Combines parameter and request body schemas into a single schema.
|
|
@@ -1388,6 +1469,19 @@ def extract_output_schema_from_responses(
|
|
| 1388 |
# Clean and copy the schema
|
| 1389 |
output_schema = schema.copy()
|
| 1390 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1391 |
# MCP requires output schemas to be objects. If this schema is not an object,
|
| 1392 |
# we need to wrap it similar to how ParsedFunction.from_function() does it
|
| 1393 |
if output_schema.get("type") != "object":
|
|
@@ -1400,9 +1494,13 @@ def extract_output_schema_from_responses(
|
|
| 1400 |
}
|
| 1401 |
output_schema = wrapped_schema
|
| 1402 |
|
| 1403 |
-
# Add schema definitions if available
|
| 1404 |
-
if
|
| 1405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1406 |
|
| 1407 |
# Use lightweight compression - prune additionalProperties and unused definitions
|
| 1408 |
if output_schema.get("additionalProperties") is False:
|
|
|
|
| 187 |
"parse_openapi_to_http_routes",
|
| 188 |
"extract_output_schema_from_responses",
|
| 189 |
"format_deep_object_parameter",
|
| 190 |
+
"_handle_nullable_fields",
|
| 191 |
]
|
| 192 |
|
| 193 |
# Type variables for generic parser
|
|
|
|
| 1170 |
return schema
|
| 1171 |
|
| 1172 |
|
| 1173 |
+
def _add_null_to_type(schema: dict[str, Any]) -> None:
|
| 1174 |
+
"""Add 'null' to the schema's type field or handle oneOf/anyOf/allOf constructs if not already present."""
|
| 1175 |
+
if "type" in schema:
|
| 1176 |
+
current_type = schema["type"]
|
| 1177 |
+
|
| 1178 |
+
if isinstance(current_type, str):
|
| 1179 |
+
# Convert string type to array with null
|
| 1180 |
+
schema["type"] = [current_type, "null"]
|
| 1181 |
+
elif isinstance(current_type, list):
|
| 1182 |
+
# Add null to array if not already present
|
| 1183 |
+
if "null" not in current_type:
|
| 1184 |
+
schema["type"] = current_type + ["null"]
|
| 1185 |
+
elif "oneOf" in schema:
|
| 1186 |
+
# Convert oneOf to anyOf with null type
|
| 1187 |
+
schema["anyOf"] = schema.pop("oneOf") + [{"type": "null"}]
|
| 1188 |
+
elif "anyOf" in schema:
|
| 1189 |
+
# Add null type to anyOf if not already present
|
| 1190 |
+
if not any(item.get("type") == "null" for item in schema["anyOf"]):
|
| 1191 |
+
schema["anyOf"].append({"type": "null"})
|
| 1192 |
+
elif "allOf" in schema:
|
| 1193 |
+
# For allOf, wrap in anyOf with null - this means (all conditions) OR null
|
| 1194 |
+
schema["anyOf"] = [{"allOf": schema.pop("allOf")}, {"type": "null"}]
|
| 1195 |
+
|
| 1196 |
+
|
| 1197 |
+
def _handle_nullable_fields(schema: dict[str, Any] | Any) -> dict[str, Any] | Any:
|
| 1198 |
+
"""Convert OpenAPI nullable fields to JSON Schema format: {"type": "string",
|
| 1199 |
+
"nullable": true} -> {"type": ["string", "null"]}"""
|
| 1200 |
+
|
| 1201 |
+
if not isinstance(schema, dict):
|
| 1202 |
+
return schema
|
| 1203 |
+
|
| 1204 |
+
# Check if we need to modify anything first to avoid unnecessary copying
|
| 1205 |
+
has_root_nullable_field = "nullable" in schema
|
| 1206 |
+
has_root_nullable_true = (
|
| 1207 |
+
has_root_nullable_field
|
| 1208 |
+
and schema["nullable"]
|
| 1209 |
+
and (
|
| 1210 |
+
"type" in schema
|
| 1211 |
+
or "oneOf" in schema
|
| 1212 |
+
or "anyOf" in schema
|
| 1213 |
+
or "allOf" in schema
|
| 1214 |
+
)
|
| 1215 |
+
)
|
| 1216 |
+
|
| 1217 |
+
has_property_nullable_field = False
|
| 1218 |
+
if "properties" in schema:
|
| 1219 |
+
for prop_schema in schema["properties"].values():
|
| 1220 |
+
if isinstance(prop_schema, dict) and "nullable" in prop_schema:
|
| 1221 |
+
has_property_nullable_field = True
|
| 1222 |
+
break
|
| 1223 |
+
|
| 1224 |
+
# If no nullable fields at all, return original schema unchanged
|
| 1225 |
+
if not has_root_nullable_field and not has_property_nullable_field:
|
| 1226 |
+
return schema
|
| 1227 |
+
|
| 1228 |
+
# Only copy if we need to modify
|
| 1229 |
+
result = schema.copy()
|
| 1230 |
+
|
| 1231 |
+
# Handle root level nullable - always remove the field, convert type if true
|
| 1232 |
+
if has_root_nullable_field:
|
| 1233 |
+
result.pop("nullable")
|
| 1234 |
+
if has_root_nullable_true:
|
| 1235 |
+
_add_null_to_type(result)
|
| 1236 |
+
|
| 1237 |
+
# Handle properties nullable fields
|
| 1238 |
+
if has_property_nullable_field and "properties" in result:
|
| 1239 |
+
for prop_name, prop_schema in result["properties"].items():
|
| 1240 |
+
if isinstance(prop_schema, dict) and "nullable" in prop_schema:
|
| 1241 |
+
nullable_value = prop_schema.pop("nullable")
|
| 1242 |
+
if nullable_value and (
|
| 1243 |
+
"type" in prop_schema
|
| 1244 |
+
or "oneOf" in prop_schema
|
| 1245 |
+
or "anyOf" in prop_schema
|
| 1246 |
+
or "allOf" in prop_schema
|
| 1247 |
+
):
|
| 1248 |
+
_add_null_to_type(prop_schema)
|
| 1249 |
+
|
| 1250 |
+
return result
|
| 1251 |
+
|
| 1252 |
+
|
| 1253 |
def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
|
| 1254 |
"""
|
| 1255 |
Combines parameter and request body schemas into a single schema.
|
|
|
|
| 1469 |
# Clean and copy the schema
|
| 1470 |
output_schema = schema.copy()
|
| 1471 |
|
| 1472 |
+
# If schema has a $ref, resolve it first before processing nullable fields
|
| 1473 |
+
if "$ref" in output_schema and schema_definitions:
|
| 1474 |
+
ref_path = output_schema["$ref"]
|
| 1475 |
+
if ref_path.startswith("#/components/schemas/"):
|
| 1476 |
+
schema_name = ref_path.split("/")[-1]
|
| 1477 |
+
if schema_name in schema_definitions:
|
| 1478 |
+
# Replace $ref with the actual schema definition
|
| 1479 |
+
output_schema = schema_definitions[schema_name].copy()
|
| 1480 |
+
|
| 1481 |
+
# Handle OpenAPI nullable fields by converting them to JSON Schema format
|
| 1482 |
+
# This prevents "None is not of type 'string'" validation errors
|
| 1483 |
+
output_schema = _handle_nullable_fields(output_schema)
|
| 1484 |
+
|
| 1485 |
# MCP requires output schemas to be objects. If this schema is not an object,
|
| 1486 |
# we need to wrap it similar to how ParsedFunction.from_function() does it
|
| 1487 |
if output_schema.get("type") != "object":
|
|
|
|
| 1494 |
}
|
| 1495 |
output_schema = wrapped_schema
|
| 1496 |
|
| 1497 |
+
# Add schema definitions if available and handle nullable fields in them
|
| 1498 |
+
# Only add $defs if we didn't resolve the $ref inline above
|
| 1499 |
+
if schema_definitions and "$ref" not in schema.copy():
|
| 1500 |
+
processed_defs = {}
|
| 1501 |
+
for def_name, def_schema in schema_definitions.items():
|
| 1502 |
+
processed_defs[def_name] = _handle_nullable_fields(def_schema)
|
| 1503 |
+
output_schema["$defs"] = processed_defs
|
| 1504 |
|
| 1505 |
# Use lightweight compression - prune additionalProperties and unused definitions
|
| 1506 |
if output_schema.get("additionalProperties") is False:
|
tests/experimental/utilities/openapi/test_nullable_fields.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for nullable field handling in OpenAPI schemas."""
|
| 2 |
+
|
| 3 |
+
from fastmcp.experimental.utilities.openapi.schemas import _handle_nullable_fields
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class TestHandleNullableFields:
|
| 7 |
+
"""Test conversion of OpenAPI nullable fields to JSON Schema format."""
|
| 8 |
+
|
| 9 |
+
def test_root_level_nullable_string(self):
|
| 10 |
+
"""Test nullable string at root level."""
|
| 11 |
+
input_schema = {"type": "string", "nullable": True}
|
| 12 |
+
expected = {"type": ["string", "null"]}
|
| 13 |
+
result = _handle_nullable_fields(input_schema)
|
| 14 |
+
assert result == expected
|
| 15 |
+
|
| 16 |
+
def test_root_level_nullable_integer(self):
|
| 17 |
+
"""Test nullable integer at root level."""
|
| 18 |
+
input_schema = {"type": "integer", "nullable": True}
|
| 19 |
+
expected = {"type": ["integer", "null"]}
|
| 20 |
+
result = _handle_nullable_fields(input_schema)
|
| 21 |
+
assert result == expected
|
| 22 |
+
|
| 23 |
+
def test_root_level_nullable_boolean(self):
|
| 24 |
+
"""Test nullable boolean at root level."""
|
| 25 |
+
input_schema = {"type": "boolean", "nullable": True}
|
| 26 |
+
expected = {"type": ["boolean", "null"]}
|
| 27 |
+
result = _handle_nullable_fields(input_schema)
|
| 28 |
+
assert result == expected
|
| 29 |
+
|
| 30 |
+
def test_property_level_nullable_fields(self):
|
| 31 |
+
"""Test nullable fields in properties."""
|
| 32 |
+
input_schema = {
|
| 33 |
+
"type": "object",
|
| 34 |
+
"properties": {
|
| 35 |
+
"name": {"type": "string"},
|
| 36 |
+
"company": {"type": "string", "nullable": True},
|
| 37 |
+
"age": {"type": "integer", "nullable": True},
|
| 38 |
+
"active": {"type": "boolean", "nullable": True},
|
| 39 |
+
},
|
| 40 |
+
}
|
| 41 |
+
expected = {
|
| 42 |
+
"type": "object",
|
| 43 |
+
"properties": {
|
| 44 |
+
"name": {"type": "string"},
|
| 45 |
+
"company": {"type": ["string", "null"]},
|
| 46 |
+
"age": {"type": ["integer", "null"]},
|
| 47 |
+
"active": {"type": ["boolean", "null"]},
|
| 48 |
+
},
|
| 49 |
+
}
|
| 50 |
+
result = _handle_nullable_fields(input_schema)
|
| 51 |
+
assert result == expected
|
| 52 |
+
|
| 53 |
+
def test_mixed_nullable_and_non_nullable(self):
|
| 54 |
+
"""Test mix of nullable and non-nullable fields."""
|
| 55 |
+
input_schema = {
|
| 56 |
+
"type": "object",
|
| 57 |
+
"properties": {
|
| 58 |
+
"required_field": {"type": "string"},
|
| 59 |
+
"optional_nullable": {"type": "string", "nullable": True},
|
| 60 |
+
"optional_non_nullable": {"type": "string"},
|
| 61 |
+
},
|
| 62 |
+
"required": ["required_field"],
|
| 63 |
+
}
|
| 64 |
+
expected = {
|
| 65 |
+
"type": "object",
|
| 66 |
+
"properties": {
|
| 67 |
+
"required_field": {"type": "string"},
|
| 68 |
+
"optional_nullable": {"type": ["string", "null"]},
|
| 69 |
+
"optional_non_nullable": {"type": "string"},
|
| 70 |
+
},
|
| 71 |
+
"required": ["required_field"],
|
| 72 |
+
}
|
| 73 |
+
result = _handle_nullable_fields(input_schema)
|
| 74 |
+
assert result == expected
|
| 75 |
+
|
| 76 |
+
def test_nullable_false_ignored(self):
|
| 77 |
+
"""Test that nullable: false is ignored (removed but no type change)."""
|
| 78 |
+
input_schema = {"type": "string", "nullable": False}
|
| 79 |
+
expected = {"type": "string"}
|
| 80 |
+
result = _handle_nullable_fields(input_schema)
|
| 81 |
+
assert result == expected
|
| 82 |
+
|
| 83 |
+
def test_no_nullable_field_unchanged(self):
|
| 84 |
+
"""Test that schemas without nullable field are unchanged."""
|
| 85 |
+
input_schema = {
|
| 86 |
+
"type": "object",
|
| 87 |
+
"properties": {"name": {"type": "string"}},
|
| 88 |
+
}
|
| 89 |
+
expected = input_schema.copy()
|
| 90 |
+
result = _handle_nullable_fields(input_schema)
|
| 91 |
+
assert result == expected
|
| 92 |
+
|
| 93 |
+
def test_nullable_without_type_removes_nullable(self):
|
| 94 |
+
"""Test that nullable field is removed even without type."""
|
| 95 |
+
input_schema = {"nullable": True, "description": "Some field"}
|
| 96 |
+
expected = {"description": "Some field"}
|
| 97 |
+
result = _handle_nullable_fields(input_schema)
|
| 98 |
+
assert result == expected
|
| 99 |
+
|
| 100 |
+
def test_preserves_other_fields(self):
|
| 101 |
+
"""Test that other fields are preserved during conversion."""
|
| 102 |
+
input_schema = {
|
| 103 |
+
"type": "string",
|
| 104 |
+
"nullable": True,
|
| 105 |
+
"description": "A nullable string",
|
| 106 |
+
"example": "test",
|
| 107 |
+
"format": "email",
|
| 108 |
+
}
|
| 109 |
+
expected = {
|
| 110 |
+
"type": ["string", "null"],
|
| 111 |
+
"description": "A nullable string",
|
| 112 |
+
"example": "test",
|
| 113 |
+
"format": "email",
|
| 114 |
+
}
|
| 115 |
+
result = _handle_nullable_fields(input_schema)
|
| 116 |
+
assert result == expected
|
| 117 |
+
|
| 118 |
+
def test_non_dict_input_unchanged(self):
|
| 119 |
+
"""Test that non-dict inputs are returned unchanged."""
|
| 120 |
+
assert _handle_nullable_fields("string") == "string" # type: ignore[arg-type]
|
| 121 |
+
assert _handle_nullable_fields(123) == 123 # type: ignore[arg-type]
|
| 122 |
+
assert _handle_nullable_fields(None) is None # type: ignore[arg-type]
|
| 123 |
+
assert _handle_nullable_fields([1, 2, 3]) == [1, 2, 3] # type: ignore[arg-type]
|
| 124 |
+
|
| 125 |
+
def test_performance_optimization_no_copy_when_unchanged(self):
|
| 126 |
+
"""Test that schemas without nullable fields return the same object (no copy)."""
|
| 127 |
+
input_schema = {
|
| 128 |
+
"type": "object",
|
| 129 |
+
"properties": {"name": {"type": "string"}},
|
| 130 |
+
}
|
| 131 |
+
result = _handle_nullable_fields(input_schema)
|
| 132 |
+
# Should return the exact same object, not a copy
|
| 133 |
+
assert result is input_schema
|
| 134 |
+
|
| 135 |
+
def test_union_types_with_nullable(self):
|
| 136 |
+
"""Test nullable handling with existing union types (type as array)."""
|
| 137 |
+
input_schema = {"type": ["string", "integer"], "nullable": True}
|
| 138 |
+
expected = {"type": ["string", "integer", "null"]}
|
| 139 |
+
result = _handle_nullable_fields(input_schema)
|
| 140 |
+
assert result == expected
|
| 141 |
+
|
| 142 |
+
def test_already_nullable_union_unchanged(self):
|
| 143 |
+
"""Test that union types already containing null are not modified."""
|
| 144 |
+
input_schema = {"type": ["string", "null"], "nullable": True}
|
| 145 |
+
expected = {"type": ["string", "null"]}
|
| 146 |
+
result = _handle_nullable_fields(input_schema)
|
| 147 |
+
assert result == expected
|
| 148 |
+
|
| 149 |
+
def test_property_level_union_with_nullable(self):
|
| 150 |
+
"""Test nullable handling with union types in properties."""
|
| 151 |
+
input_schema = {
|
| 152 |
+
"type": "object",
|
| 153 |
+
"properties": {"value": {"type": ["string", "integer"], "nullable": True}},
|
| 154 |
+
}
|
| 155 |
+
expected = {
|
| 156 |
+
"type": "object",
|
| 157 |
+
"properties": {"value": {"type": ["string", "integer", "null"]}},
|
| 158 |
+
}
|
| 159 |
+
result = _handle_nullable_fields(input_schema)
|
| 160 |
+
assert result == expected
|
| 161 |
+
|
| 162 |
+
def test_complex_union_nullable_scenarios(self):
|
| 163 |
+
"""Test various complex union type scenarios."""
|
| 164 |
+
# Already has null in different position
|
| 165 |
+
input1 = {"type": ["null", "string", "integer"], "nullable": True}
|
| 166 |
+
result1 = _handle_nullable_fields(input1)
|
| 167 |
+
assert result1 == {"type": ["null", "string", "integer"]}
|
| 168 |
+
|
| 169 |
+
# Single item array
|
| 170 |
+
input2 = {"type": ["string"], "nullable": True}
|
| 171 |
+
result2 = _handle_nullable_fields(input2)
|
| 172 |
+
assert result2 == {"type": ["string", "null"]}
|
| 173 |
+
|
| 174 |
+
def test_oneof_with_nullable(self):
|
| 175 |
+
"""Test nullable handling with oneOf constructs."""
|
| 176 |
+
input_schema = {
|
| 177 |
+
"oneOf": [{"type": "string"}, {"type": "integer"}],
|
| 178 |
+
"nullable": True,
|
| 179 |
+
}
|
| 180 |
+
expected = {
|
| 181 |
+
"anyOf": [{"type": "string"}, {"type": "integer"}, {"type": "null"}]
|
| 182 |
+
}
|
| 183 |
+
result = _handle_nullable_fields(input_schema)
|
| 184 |
+
assert result == expected
|
| 185 |
+
|
| 186 |
+
def test_anyof_with_nullable(self):
|
| 187 |
+
"""Test nullable handling with anyOf constructs."""
|
| 188 |
+
input_schema = {
|
| 189 |
+
"anyOf": [{"type": "string"}, {"type": "integer"}],
|
| 190 |
+
"nullable": True,
|
| 191 |
+
}
|
| 192 |
+
expected = {
|
| 193 |
+
"anyOf": [{"type": "string"}, {"type": "integer"}, {"type": "null"}]
|
| 194 |
+
}
|
| 195 |
+
result = _handle_nullable_fields(input_schema)
|
| 196 |
+
assert result == expected
|
| 197 |
+
|
| 198 |
+
def test_anyof_already_nullable(self):
|
| 199 |
+
"""Test anyOf that already contains null type."""
|
| 200 |
+
input_schema = {
|
| 201 |
+
"anyOf": [{"type": "string"}, {"type": "null"}],
|
| 202 |
+
"nullable": True,
|
| 203 |
+
}
|
| 204 |
+
expected = {"anyOf": [{"type": "string"}, {"type": "null"}]}
|
| 205 |
+
result = _handle_nullable_fields(input_schema)
|
| 206 |
+
assert result == expected
|
| 207 |
+
|
| 208 |
+
def test_allof_with_nullable(self):
|
| 209 |
+
"""Test nullable handling with allOf constructs."""
|
| 210 |
+
input_schema = {
|
| 211 |
+
"allOf": [{"type": "string"}, {"minLength": 1}],
|
| 212 |
+
"nullable": True,
|
| 213 |
+
}
|
| 214 |
+
expected = {
|
| 215 |
+
"anyOf": [
|
| 216 |
+
{"allOf": [{"type": "string"}, {"minLength": 1}]},
|
| 217 |
+
{"type": "null"},
|
| 218 |
+
]
|
| 219 |
+
}
|
| 220 |
+
result = _handle_nullable_fields(input_schema)
|
| 221 |
+
assert result == expected
|
| 222 |
+
|
| 223 |
+
def test_property_level_oneof_with_nullable(self):
|
| 224 |
+
"""Test nullable handling with oneOf in properties."""
|
| 225 |
+
input_schema = {
|
| 226 |
+
"type": "object",
|
| 227 |
+
"properties": {
|
| 228 |
+
"value": {
|
| 229 |
+
"oneOf": [{"type": "string"}, {"type": "integer"}],
|
| 230 |
+
"nullable": True,
|
| 231 |
+
}
|
| 232 |
+
},
|
| 233 |
+
}
|
| 234 |
+
expected = {
|
| 235 |
+
"type": "object",
|
| 236 |
+
"properties": {
|
| 237 |
+
"value": {
|
| 238 |
+
"anyOf": [{"type": "string"}, {"type": "integer"}, {"type": "null"}]
|
| 239 |
+
}
|
| 240 |
+
},
|
| 241 |
+
}
|
| 242 |
+
result = _handle_nullable_fields(input_schema)
|
| 243 |
+
assert result == expected
|
tests/utilities/openapi/test_nullable_fields.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for nullable field handling in OpenAPI schemas."""
|
| 2 |
+
|
| 3 |
+
from fastmcp.utilities.openapi import _handle_nullable_fields
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class TestHandleNullableFields:
|
| 7 |
+
"""Test conversion of OpenAPI nullable fields to JSON Schema format."""
|
| 8 |
+
|
| 9 |
+
def test_root_level_nullable_string(self):
|
| 10 |
+
"""Test nullable string at root level."""
|
| 11 |
+
input_schema = {"type": "string", "nullable": True}
|
| 12 |
+
expected = {"type": ["string", "null"]}
|
| 13 |
+
result = _handle_nullable_fields(input_schema)
|
| 14 |
+
assert result == expected
|
| 15 |
+
|
| 16 |
+
def test_root_level_nullable_integer(self):
|
| 17 |
+
"""Test nullable integer at root level."""
|
| 18 |
+
input_schema = {"type": "integer", "nullable": True}
|
| 19 |
+
expected = {"type": ["integer", "null"]}
|
| 20 |
+
result = _handle_nullable_fields(input_schema)
|
| 21 |
+
assert result == expected
|
| 22 |
+
|
| 23 |
+
def test_root_level_nullable_boolean(self):
|
| 24 |
+
"""Test nullable boolean at root level."""
|
| 25 |
+
input_schema = {"type": "boolean", "nullable": True}
|
| 26 |
+
expected = {"type": ["boolean", "null"]}
|
| 27 |
+
result = _handle_nullable_fields(input_schema)
|
| 28 |
+
assert result == expected
|
| 29 |
+
|
| 30 |
+
def test_property_level_nullable_fields(self):
|
| 31 |
+
"""Test nullable fields in properties."""
|
| 32 |
+
input_schema = {
|
| 33 |
+
"type": "object",
|
| 34 |
+
"properties": {
|
| 35 |
+
"name": {"type": "string"},
|
| 36 |
+
"company": {"type": "string", "nullable": True},
|
| 37 |
+
"age": {"type": "integer", "nullable": True},
|
| 38 |
+
"active": {"type": "boolean", "nullable": True},
|
| 39 |
+
},
|
| 40 |
+
}
|
| 41 |
+
expected = {
|
| 42 |
+
"type": "object",
|
| 43 |
+
"properties": {
|
| 44 |
+
"name": {"type": "string"},
|
| 45 |
+
"company": {"type": ["string", "null"]},
|
| 46 |
+
"age": {"type": ["integer", "null"]},
|
| 47 |
+
"active": {"type": ["boolean", "null"]},
|
| 48 |
+
},
|
| 49 |
+
}
|
| 50 |
+
result = _handle_nullable_fields(input_schema)
|
| 51 |
+
assert result == expected
|
| 52 |
+
|
| 53 |
+
def test_mixed_nullable_and_non_nullable(self):
|
| 54 |
+
"""Test mix of nullable and non-nullable fields."""
|
| 55 |
+
input_schema = {
|
| 56 |
+
"type": "object",
|
| 57 |
+
"properties": {
|
| 58 |
+
"required_field": {"type": "string"},
|
| 59 |
+
"optional_nullable": {"type": "string", "nullable": True},
|
| 60 |
+
"optional_non_nullable": {"type": "string"},
|
| 61 |
+
},
|
| 62 |
+
"required": ["required_field"],
|
| 63 |
+
}
|
| 64 |
+
expected = {
|
| 65 |
+
"type": "object",
|
| 66 |
+
"properties": {
|
| 67 |
+
"required_field": {"type": "string"},
|
| 68 |
+
"optional_nullable": {"type": ["string", "null"]},
|
| 69 |
+
"optional_non_nullable": {"type": "string"},
|
| 70 |
+
},
|
| 71 |
+
"required": ["required_field"],
|
| 72 |
+
}
|
| 73 |
+
result = _handle_nullable_fields(input_schema)
|
| 74 |
+
assert result == expected
|
| 75 |
+
|
| 76 |
+
def test_nullable_false_ignored(self):
|
| 77 |
+
"""Test that nullable: false is ignored (removed but no type change)."""
|
| 78 |
+
input_schema = {"type": "string", "nullable": False}
|
| 79 |
+
expected = {"type": "string"}
|
| 80 |
+
result = _handle_nullable_fields(input_schema)
|
| 81 |
+
assert result == expected
|
| 82 |
+
|
| 83 |
+
def test_no_nullable_field_unchanged(self):
|
| 84 |
+
"""Test that schemas without nullable field are unchanged."""
|
| 85 |
+
input_schema = {
|
| 86 |
+
"type": "object",
|
| 87 |
+
"properties": {"name": {"type": "string"}},
|
| 88 |
+
}
|
| 89 |
+
expected = input_schema.copy()
|
| 90 |
+
result = _handle_nullable_fields(input_schema)
|
| 91 |
+
assert result == expected
|
| 92 |
+
|
| 93 |
+
def test_nullable_without_type_removes_nullable(self):
|
| 94 |
+
"""Test that nullable field is removed even without type."""
|
| 95 |
+
input_schema = {"nullable": True, "description": "Some field"}
|
| 96 |
+
expected = {"description": "Some field"}
|
| 97 |
+
result = _handle_nullable_fields(input_schema)
|
| 98 |
+
assert result == expected
|
| 99 |
+
|
| 100 |
+
def test_preserves_other_fields(self):
|
| 101 |
+
"""Test that other fields are preserved during conversion."""
|
| 102 |
+
input_schema = {
|
| 103 |
+
"type": "string",
|
| 104 |
+
"nullable": True,
|
| 105 |
+
"description": "A nullable string",
|
| 106 |
+
"example": "test",
|
| 107 |
+
"format": "email",
|
| 108 |
+
}
|
| 109 |
+
expected = {
|
| 110 |
+
"type": ["string", "null"],
|
| 111 |
+
"description": "A nullable string",
|
| 112 |
+
"example": "test",
|
| 113 |
+
"format": "email",
|
| 114 |
+
}
|
| 115 |
+
result = _handle_nullable_fields(input_schema)
|
| 116 |
+
assert result == expected
|
| 117 |
+
|
| 118 |
+
def test_non_dict_input_unchanged(self):
|
| 119 |
+
"""Test that non-dict inputs are returned unchanged."""
|
| 120 |
+
assert _handle_nullable_fields("string") == "string" # type: ignore[arg-type]
|
| 121 |
+
assert _handle_nullable_fields(123) == 123 # type: ignore[arg-type]
|
| 122 |
+
assert _handle_nullable_fields(None) is None # type: ignore[arg-type]
|
| 123 |
+
assert _handle_nullable_fields([1, 2, 3]) == [1, 2, 3] # type: ignore[arg-type]
|
| 124 |
+
|
| 125 |
+
def test_performance_optimization_no_copy_when_unchanged(self):
|
| 126 |
+
"""Test that schemas without nullable fields return the same object (no copy)."""
|
| 127 |
+
input_schema = {
|
| 128 |
+
"type": "object",
|
| 129 |
+
"properties": {"name": {"type": "string"}},
|
| 130 |
+
}
|
| 131 |
+
result = _handle_nullable_fields(input_schema)
|
| 132 |
+
# Should return the exact same object, not a copy
|
| 133 |
+
assert result is input_schema
|
| 134 |
+
|
| 135 |
+
def test_union_types_with_nullable(self):
|
| 136 |
+
"""Test nullable handling with existing union types (type as array)."""
|
| 137 |
+
input_schema = {"type": ["string", "integer"], "nullable": True}
|
| 138 |
+
expected = {"type": ["string", "integer", "null"]}
|
| 139 |
+
result = _handle_nullable_fields(input_schema)
|
| 140 |
+
assert result == expected
|
| 141 |
+
|
| 142 |
+
def test_already_nullable_union_unchanged(self):
|
| 143 |
+
"""Test that union types already containing null are not modified."""
|
| 144 |
+
input_schema = {"type": ["string", "null"], "nullable": True}
|
| 145 |
+
expected = {"type": ["string", "null"]}
|
| 146 |
+
result = _handle_nullable_fields(input_schema)
|
| 147 |
+
assert result == expected
|
| 148 |
+
|
| 149 |
+
def test_property_level_union_with_nullable(self):
|
| 150 |
+
"""Test nullable handling with union types in properties."""
|
| 151 |
+
input_schema = {
|
| 152 |
+
"type": "object",
|
| 153 |
+
"properties": {"value": {"type": ["string", "integer"], "nullable": True}},
|
| 154 |
+
}
|
| 155 |
+
expected = {
|
| 156 |
+
"type": "object",
|
| 157 |
+
"properties": {"value": {"type": ["string", "integer", "null"]}},
|
| 158 |
+
}
|
| 159 |
+
result = _handle_nullable_fields(input_schema)
|
| 160 |
+
assert result == expected
|
| 161 |
+
|
| 162 |
+
def test_complex_union_nullable_scenarios(self):
|
| 163 |
+
"""Test various complex union type scenarios."""
|
| 164 |
+
# Already has null in different position
|
| 165 |
+
input1 = {"type": ["null", "string", "integer"], "nullable": True}
|
| 166 |
+
result1 = _handle_nullable_fields(input1)
|
| 167 |
+
assert result1 == {"type": ["null", "string", "integer"]}
|
| 168 |
+
|
| 169 |
+
# Single item array
|
| 170 |
+
input2 = {"type": ["string"], "nullable": True}
|
| 171 |
+
result2 = _handle_nullable_fields(input2)
|
| 172 |
+
assert result2 == {"type": ["string", "null"]}
|
| 173 |
+
|
| 174 |
+
def test_oneof_with_nullable(self):
|
| 175 |
+
"""Test nullable handling with oneOf constructs."""
|
| 176 |
+
input_schema = {
|
| 177 |
+
"oneOf": [{"type": "string"}, {"type": "integer"}],
|
| 178 |
+
"nullable": True,
|
| 179 |
+
}
|
| 180 |
+
expected = {
|
| 181 |
+
"anyOf": [{"type": "string"}, {"type": "integer"}, {"type": "null"}]
|
| 182 |
+
}
|
| 183 |
+
result = _handle_nullable_fields(input_schema)
|
| 184 |
+
assert result == expected
|
| 185 |
+
|
| 186 |
+
def test_anyof_with_nullable(self):
|
| 187 |
+
"""Test nullable handling with anyOf constructs."""
|
| 188 |
+
input_schema = {
|
| 189 |
+
"anyOf": [{"type": "string"}, {"type": "integer"}],
|
| 190 |
+
"nullable": True,
|
| 191 |
+
}
|
| 192 |
+
expected = {
|
| 193 |
+
"anyOf": [{"type": "string"}, {"type": "integer"}, {"type": "null"}]
|
| 194 |
+
}
|
| 195 |
+
result = _handle_nullable_fields(input_schema)
|
| 196 |
+
assert result == expected
|
| 197 |
+
|
| 198 |
+
def test_anyof_already_nullable(self):
|
| 199 |
+
"""Test anyOf that already contains null type."""
|
| 200 |
+
input_schema = {
|
| 201 |
+
"anyOf": [{"type": "string"}, {"type": "null"}],
|
| 202 |
+
"nullable": True,
|
| 203 |
+
}
|
| 204 |
+
expected = {"anyOf": [{"type": "string"}, {"type": "null"}]}
|
| 205 |
+
result = _handle_nullable_fields(input_schema)
|
| 206 |
+
assert result == expected
|
| 207 |
+
|
| 208 |
+
def test_allof_with_nullable(self):
|
| 209 |
+
"""Test nullable handling with allOf constructs."""
|
| 210 |
+
input_schema = {
|
| 211 |
+
"allOf": [{"type": "string"}, {"minLength": 1}],
|
| 212 |
+
"nullable": True,
|
| 213 |
+
}
|
| 214 |
+
expected = {
|
| 215 |
+
"anyOf": [
|
| 216 |
+
{"allOf": [{"type": "string"}, {"minLength": 1}]},
|
| 217 |
+
{"type": "null"},
|
| 218 |
+
]
|
| 219 |
+
}
|
| 220 |
+
result = _handle_nullable_fields(input_schema)
|
| 221 |
+
assert result == expected
|
| 222 |
+
|
| 223 |
+
def test_property_level_oneof_with_nullable(self):
|
| 224 |
+
"""Test nullable handling with oneOf in properties."""
|
| 225 |
+
input_schema = {
|
| 226 |
+
"type": "object",
|
| 227 |
+
"properties": {
|
| 228 |
+
"value": {
|
| 229 |
+
"oneOf": [{"type": "string"}, {"type": "integer"}],
|
| 230 |
+
"nullable": True,
|
| 231 |
+
}
|
| 232 |
+
},
|
| 233 |
+
}
|
| 234 |
+
expected = {
|
| 235 |
+
"type": "object",
|
| 236 |
+
"properties": {
|
| 237 |
+
"value": {
|
| 238 |
+
"anyOf": [{"type": "string"}, {"type": "integer"}, {"type": "null"}]
|
| 239 |
+
}
|
| 240 |
+
},
|
| 241 |
+
}
|
| 242 |
+
result = _handle_nullable_fields(input_schema)
|
| 243 |
+
assert result == expected
|