Spaces:
Running
Running
| """ | |
| Clean OpenAPI 3.0 to JSON Schema converter for the experimental parser. | |
| This module provides a systematic approach to converting OpenAPI 3.0 schemas | |
| to JSON Schema, inspired by py-openapi-schema-to-json-schema but optimized | |
| for our specific use case. | |
| """ | |
| from typing import Any | |
| from fastmcp.utilities.logging import get_logger | |
| logger = get_logger(__name__) | |
| # OpenAPI-specific fields that should be removed from JSON Schema | |
| OPENAPI_SPECIFIC_FIELDS = { | |
| "nullable", # Handled by converting to type arrays | |
| "discriminator", # OpenAPI-specific | |
| "readOnly", # OpenAPI-specific metadata | |
| "writeOnly", # OpenAPI-specific metadata | |
| "xml", # OpenAPI-specific metadata | |
| "externalDocs", # OpenAPI-specific metadata | |
| "deprecated", # Can be kept but not part of JSON Schema core | |
| } | |
| # Fields that should be recursively processed | |
| RECURSIVE_FIELDS = { | |
| "properties": dict, | |
| "items": dict, | |
| "additionalProperties": dict, | |
| "allOf": list, | |
| "anyOf": list, | |
| "oneOf": list, | |
| "not": dict, | |
| } | |
| def convert_openapi_schema_to_json_schema( | |
| schema: dict[str, Any], | |
| openapi_version: str | None = None, | |
| remove_read_only: bool = False, | |
| remove_write_only: bool = False, | |
| convert_one_of_to_any_of: bool = True, | |
| ) -> dict[str, Any]: | |
| """ | |
| Convert an OpenAPI schema to JSON Schema format. | |
| This is a clean, systematic approach that: | |
| 1. Removes OpenAPI-specific fields | |
| 2. Converts nullable fields to type arrays (for OpenAPI 3.0 only) | |
| 3. Converts oneOf to anyOf for overlapping union handling | |
| 4. Recursively processes nested schemas | |
| 5. Optionally removes readOnly/writeOnly properties | |
| Args: | |
| schema: OpenAPI schema dictionary | |
| openapi_version: OpenAPI version for optimization | |
| remove_read_only: Whether to remove readOnly properties | |
| remove_write_only: Whether to remove writeOnly properties | |
| convert_one_of_to_any_of: Whether to convert oneOf to anyOf | |
| Returns: | |
| JSON Schema compatible dictionary | |
| """ | |
| if not isinstance(schema, dict): | |
| return schema | |
| # Early exit optimization - check if conversion is needed | |
| needs_conversion = ( | |
| any(field in schema for field in OPENAPI_SPECIFIC_FIELDS) | |
| or (remove_read_only and _has_read_only_properties(schema)) | |
| or (remove_write_only and _has_write_only_properties(schema)) | |
| or (convert_one_of_to_any_of and "oneOf" in schema) | |
| or _needs_recursive_processing( | |
| schema, | |
| openapi_version, | |
| remove_read_only, | |
| remove_write_only, | |
| convert_one_of_to_any_of, | |
| ) | |
| ) | |
| if not needs_conversion: | |
| return schema | |
| # Work on a copy to avoid mutation | |
| result = schema.copy() | |
| # Step 1: Handle nullable field conversion (OpenAPI 3.0 only) | |
| if openapi_version and openapi_version.startswith("3.0"): | |
| result = _convert_nullable_field(result) | |
| # Step 2: Convert oneOf to anyOf if requested | |
| if convert_one_of_to_any_of and "oneOf" in result: | |
| result["anyOf"] = result.pop("oneOf") | |
| # Step 3: Remove OpenAPI-specific fields | |
| for field in OPENAPI_SPECIFIC_FIELDS: | |
| result.pop(field, None) | |
| # Step 4: Handle readOnly/writeOnly property removal | |
| if remove_read_only or remove_write_only: | |
| result = _filter_properties_by_access( | |
| result, remove_read_only, remove_write_only | |
| ) | |
| # Step 5: Recursively process nested schemas | |
| for field_name, field_type in RECURSIVE_FIELDS.items(): | |
| if field_name in result: | |
| if field_type is dict and isinstance(result[field_name], dict): | |
| if field_name == "properties": | |
| # Handle properties specially - each property is a schema | |
| result[field_name] = { | |
| prop_name: convert_openapi_schema_to_json_schema( | |
| prop_schema, | |
| openapi_version, | |
| remove_read_only, | |
| remove_write_only, | |
| convert_one_of_to_any_of, | |
| ) | |
| if isinstance(prop_schema, dict) | |
| else prop_schema | |
| for prop_name, prop_schema in result[field_name].items() | |
| } | |
| else: | |
| result[field_name] = convert_openapi_schema_to_json_schema( | |
| result[field_name], | |
| openapi_version, | |
| remove_read_only, | |
| remove_write_only, | |
| convert_one_of_to_any_of, | |
| ) | |
| elif field_type is list and isinstance(result[field_name], list): | |
| result[field_name] = [ | |
| convert_openapi_schema_to_json_schema( | |
| item, | |
| openapi_version, | |
| remove_read_only, | |
| remove_write_only, | |
| convert_one_of_to_any_of, | |
| ) | |
| if isinstance(item, dict) | |
| else item | |
| for item in result[field_name] | |
| ] | |
| return result | |
| def _convert_nullable_field(schema: dict[str, Any]) -> dict[str, Any]: | |
| """Convert OpenAPI nullable field to JSON Schema type array.""" | |
| if "nullable" not in schema: | |
| return schema | |
| result = schema.copy() | |
| nullable_value = result.pop("nullable") | |
| # Only convert if nullable is True and we have a type structure | |
| if not nullable_value: | |
| return result | |
| if "type" in result: | |
| current_type = result["type"] | |
| if isinstance(current_type, str): | |
| result["type"] = [current_type, "null"] | |
| elif isinstance(current_type, list) and "null" not in current_type: | |
| result["type"] = current_type + ["null"] | |
| elif "oneOf" in result: | |
| # Convert oneOf to anyOf with null | |
| result["anyOf"] = result.pop("oneOf") + [{"type": "null"}] | |
| elif "anyOf" in result: | |
| # Add null to anyOf if not present | |
| if not any(item.get("type") == "null" for item in result["anyOf"]): | |
| result["anyOf"].append({"type": "null"}) | |
| elif "allOf" in result: | |
| # Wrap allOf in anyOf with null option | |
| result["anyOf"] = [{"allOf": result.pop("allOf")}, {"type": "null"}] | |
| return result | |
| def _has_read_only_properties(schema: dict[str, Any]) -> bool: | |
| """Quick check if schema has any readOnly properties.""" | |
| if "properties" not in schema: | |
| return False | |
| return any( | |
| isinstance(prop, dict) and prop.get("readOnly") | |
| for prop in schema["properties"].values() | |
| ) | |
| def _has_write_only_properties(schema: dict[str, Any]) -> bool: | |
| """Quick check if schema has any writeOnly properties.""" | |
| if "properties" not in schema: | |
| return False | |
| return any( | |
| isinstance(prop, dict) and prop.get("writeOnly") | |
| for prop in schema["properties"].values() | |
| ) | |
| def _needs_recursive_processing( | |
| schema: dict[str, Any], | |
| openapi_version: str | None, | |
| remove_read_only: bool, | |
| remove_write_only: bool, | |
| convert_one_of_to_any_of: bool, | |
| ) -> bool: | |
| """Check if the schema needs recursive processing (smarter than just checking for recursive fields).""" | |
| for field_name, field_type in RECURSIVE_FIELDS.items(): | |
| if field_name in schema: | |
| if field_type is dict and isinstance(schema[field_name], dict): | |
| if field_name == "properties": | |
| # Check if any property needs conversion | |
| for prop_schema in schema[field_name].values(): | |
| if isinstance(prop_schema, dict): | |
| nested_needs_conversion = ( | |
| any( | |
| field in prop_schema | |
| for field in OPENAPI_SPECIFIC_FIELDS | |
| ) | |
| or (remove_read_only and prop_schema.get("readOnly")) | |
| or (remove_write_only and prop_schema.get("writeOnly")) | |
| or (convert_one_of_to_any_of and "oneOf" in prop_schema) | |
| or _needs_recursive_processing( | |
| prop_schema, | |
| openapi_version, | |
| remove_read_only, | |
| remove_write_only, | |
| convert_one_of_to_any_of, | |
| ) | |
| ) | |
| if nested_needs_conversion: | |
| return True | |
| else: | |
| # Check if nested schema needs conversion | |
| nested_needs_conversion = ( | |
| any( | |
| field in schema[field_name] | |
| for field in OPENAPI_SPECIFIC_FIELDS | |
| ) | |
| or ( | |
| remove_read_only | |
| and _has_read_only_properties(schema[field_name]) | |
| ) | |
| or ( | |
| remove_write_only | |
| and _has_write_only_properties(schema[field_name]) | |
| ) | |
| or (convert_one_of_to_any_of and "oneOf" in schema[field_name]) | |
| or _needs_recursive_processing( | |
| schema[field_name], | |
| openapi_version, | |
| remove_read_only, | |
| remove_write_only, | |
| convert_one_of_to_any_of, | |
| ) | |
| ) | |
| if nested_needs_conversion: | |
| return True | |
| elif field_type is list and isinstance(schema[field_name], list): | |
| # Check if any list item needs conversion | |
| for item in schema[field_name]: | |
| if isinstance(item, dict): | |
| nested_needs_conversion = ( | |
| any(field in item for field in OPENAPI_SPECIFIC_FIELDS) | |
| or (remove_read_only and _has_read_only_properties(item)) | |
| or (remove_write_only and _has_write_only_properties(item)) | |
| or (convert_one_of_to_any_of and "oneOf" in item) | |
| or _needs_recursive_processing( | |
| item, | |
| openapi_version, | |
| remove_read_only, | |
| remove_write_only, | |
| convert_one_of_to_any_of, | |
| ) | |
| ) | |
| if nested_needs_conversion: | |
| return True | |
| return False | |
| def _filter_properties_by_access( | |
| schema: dict[str, Any], remove_read_only: bool, remove_write_only: bool | |
| ) -> dict[str, Any]: | |
| """Remove readOnly and/or writeOnly properties from schema.""" | |
| if "properties" not in schema: | |
| return schema | |
| result = schema.copy() | |
| filtered_properties = {} | |
| for prop_name, prop_schema in result["properties"].items(): | |
| if not isinstance(prop_schema, dict): | |
| filtered_properties[prop_name] = prop_schema | |
| continue | |
| should_remove = (remove_read_only and prop_schema.get("readOnly")) or ( | |
| remove_write_only and prop_schema.get("writeOnly") | |
| ) | |
| if not should_remove: | |
| filtered_properties[prop_name] = prop_schema | |
| result["properties"] = filtered_properties | |
| # Clean up required array if properties were removed | |
| if "required" in result and filtered_properties: | |
| result["required"] = [ | |
| prop for prop in result["required"] if prop in filtered_properties | |
| ] | |
| if not result["required"]: | |
| result.pop("required") | |
| return result | |
| def convert_schema_definitions( | |
| schema_definitions: dict[str, Any] | None, | |
| openapi_version: str | None = None, | |
| **kwargs, | |
| ) -> dict[str, Any]: | |
| """ | |
| Convert a dictionary of OpenAPI schema definitions to JSON Schema. | |
| Args: | |
| schema_definitions: Dictionary of schema definitions | |
| openapi_version: OpenAPI version for optimization | |
| **kwargs: Additional arguments passed to convert_openapi_schema_to_json_schema | |
| Returns: | |
| Dictionary of converted schema definitions | |
| """ | |
| if not schema_definitions: | |
| return {} | |
| return { | |
| name: convert_openapi_schema_to_json_schema(schema, openapi_version, **kwargs) | |
| for name, schema in schema_definitions.items() | |
| } | |