# # Copyright 2026 The Dapr Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # from typing import Any, Dict, Optional, Type, List import logging from pydantic import BaseModel, Field, create_model logger = logging.getLogger(__name__) # Mapping from JSON Schema types to Python types TYPE_MAPPING = { "string": str, "number": float, "integer": int, "boolean": bool, "object": dict, "array": list, "null": type(None), } def create_pydantic_model_from_schema( schema: Dict[str, Any], model_name: str ) -> Type[BaseModel]: """ Create a Pydantic model from a JSON schema definition. This function converts a JSON Schema object (commonly used in MCP tool definitions) to a Pydantic model that can be used for validation in the Dapr agent framework. Args: schema: JSON Schema dictionary containing type information model_name: Name for the generated model class Returns: A dynamically created Pydantic model class Raises: ValueError: If the schema is invalid or cannot be converted """ logger.debug(f"Creating Pydantic model '{model_name}' from schema") try: properties = schema.get("properties", {}) required = set(schema.get("required", [])) # Handle schemas that wrap arguments in a 'kwargs' field # Some MCP tools use this pattern, but we want to unwrap it to accept flat arguments if ( len(properties) == 1 and "kwargs" in properties and properties["kwargs"].get("type") == "object" and "properties" in properties["kwargs"] ): logger.debug( f"Detected 'kwargs' wrapper in schema for '{model_name}', unwrapping to inner properties" ) # Use the inner schema's properties instead kwargs_schema = properties["kwargs"] properties = kwargs_schema["properties"] required = set(kwargs_schema.get("required", [])) fields = {} # Process each property in the schema for field_name, field_props in properties.items(): # --- Handle anyOf/oneOf for nullable/union fields --- if "anyOf" in field_props or "oneOf" in field_props: variants = field_props.get("anyOf") or field_props.get("oneOf") types = [v.get("type", "string") for v in variants] has_null = "null" in types non_null_variants = [v for v in variants if v.get("type") != "null"] if non_null_variants: primary_type = non_null_variants[0].get("type", "string") field_type = TYPE_MAPPING.get(primary_type, str) # Handle array/object with items/properties if primary_type == "array" and "items" in non_null_variants[0]: item_type = non_null_variants[0]["items"].get("type", "string") field_type = List[TYPE_MAPPING.get(item_type, str)] elif primary_type == "object": field_type = dict else: field_type = str if has_null: field_type = Optional[field_type] else: # --- Fallback to "type" --- json_type = field_props.get("type", "string") field_type = TYPE_MAPPING.get(json_type, str) if json_type == "array" and "items" in field_props: item_type = field_props["items"].get("type", "string") field_type = List[TYPE_MAPPING.get(item_type, str)] # Set default value based on required status if field_name in required: default = ... else: default = None # Make optional if not already if not ( hasattr(field_type, "__origin__") and field_type.__origin__ is Optional ): field_type = Optional[field_type] field_description = field_props.get("description", "") fields[field_name] = ( field_type, Field(default, description=field_description), ) # Create and return the model class return create_model(model_name, **fields) except Exception as e: logger.error(f"Failed to create model from schema: {str(e)}") raise ValueError(f"Invalid schema: {str(e)}")