2
2
3
3
import logging
4
4
from datetime import datetime
5
- from typing import Any , Dict , List , Optional , Type
5
+ from typing import Any , Dict , List , Optional , Type , Union
6
6
7
7
from pydantic import BaseModel , Field , create_model
8
8
@@ -28,52 +28,49 @@ def create_model_from_schema(
28
28
29
29
Raises:
30
30
ValueError: If schema is invalid or unsupported
31
-
32
- Example:
33
- schema = {
34
- "type": "object",
35
- "properties": {
36
- "name": {"type": "string", "description": "User name"},
37
- "age": {"type": "integer", "minimum": 0}
38
- },
39
- "required": ["name"]
40
- }
41
- UserModel = PydanticModelFactory.create_model_from_schema("User", schema)
42
31
"""
43
- if not PydanticModelFactory . validate_schema (schema ):
44
- raise ValueError (f"Invalid schema for model ' { model_name } ' " )
32
+ if not isinstance (schema , dict ):
33
+ raise ValueError (f"Schema must be a dictionary, got { type ( schema ) } " )
45
34
46
35
if schema .get ("type" ) != "object" :
47
- raise ValueError ("Schema must be of type 'object'" )
36
+ raise ValueError (f "Schema must be of type 'object', got { schema . get ( 'type' ) } " )
48
37
49
38
properties = schema .get ("properties" , {})
50
39
required_fields = set (schema .get ("required" , []))
51
40
52
- # Build field definitions
53
- field_definitions = {}
41
+ if not properties :
42
+ logger .warning ("Schema '%s' has no properties defined" , model_name )
43
+
44
+ # Build field definitions for create_model
45
+ field_definitions : Dict [str , Any ] = {}
54
46
55
47
for field_name , field_schema in properties .items ():
56
48
try :
49
+ is_required = field_name in required_fields
57
50
field_type , field_info = PydanticModelFactory ._process_field_schema (
58
- field_name , field_schema , field_name in required_fields
51
+ field_name , field_schema , is_required
59
52
)
60
53
field_definitions [field_name ] = (field_type , field_info )
61
54
except Exception as e :
62
55
logger .warning ("Error processing field '%s' in schema '%s': %s" , field_name , model_name , e )
63
56
# Use Any type as fallback
57
+ fallback_type = Optional [Any ] if field_name not in required_fields else Any
64
58
field_definitions [field_name ] = (
65
- Optional [ Any ] if field_name not in required_fields else Any ,
59
+ fallback_type ,
66
60
Field (description = f"Field processing failed: { e } " ),
67
61
)
68
62
69
63
# Create the model
70
64
try :
71
- return create_model (model_name , __base__ = base_class , ** field_definitions )
65
+ model_class = create_model (model_name , __base__ = base_class , ** field_definitions )
66
+ return model_class
72
67
except Exception as e :
73
68
raise ValueError (f"Failed to create model '{ model_name } ': { e } " ) from e
74
69
75
70
@staticmethod
76
- def _process_field_schema (field_name : str , field_schema : Dict [str , Any ], is_required : bool ) -> tuple [Type , Any ]:
71
+ def _process_field_schema (
72
+ field_name : str , field_schema : Dict [str , Any ], is_required : bool
73
+ ) -> tuple [Type [Any ], Any ]:
77
74
"""Process a single field schema into Pydantic field type and info.
78
75
79
76
Args:
@@ -88,7 +85,7 @@ def _process_field_schema(field_name: str, field_schema: Dict[str, Any], is_requ
88
85
89
86
# Handle optional fields
90
87
if not is_required :
91
- field_type = Optional [field_type ]
88
+ field_type = Optional [field_type ] # type: ignore[assignment]
92
89
93
90
# Create Field with metadata
94
91
field_kwargs = {}
@@ -120,145 +117,98 @@ def _process_field_schema(field_name: str, field_schema: Dict[str, Any], is_requ
120
117
try :
121
118
from pydantic import EmailStr
122
119
123
- field_type = EmailStr if is_required else Optional [EmailStr ]
120
+ field_type = EmailStr if is_required else Optional [EmailStr ] # type: ignore[assignment]
124
121
except ImportError :
125
- logger .warning ("EmailStr not available, using str for email format" )
122
+ logger .warning ("EmailStr not available, using str for email field '%s'" , field_name )
123
+ field_type = str if is_required else Optional [str ] # type: ignore[assignment]
126
124
elif format_type == "uri" :
127
125
try :
128
126
from pydantic import HttpUrl
129
127
130
- field_type = HttpUrl if is_required else Optional [HttpUrl ]
128
+ field_type = HttpUrl if is_required else Optional [HttpUrl ] # type: ignore[assignment]
131
129
except ImportError :
132
- logger .warning ("HttpUrl not available, using str for uri format" )
130
+ logger .warning ("HttpUrl not available, using str for uri field '%s'" , field_name )
131
+ field_type = str if is_required else Optional [str ] # type: ignore[assignment]
133
132
elif format_type == "date-time" :
134
- field_type = datetime if is_required else Optional [datetime ]
133
+ field_type = datetime if is_required else Optional [datetime ] # type: ignore[assignment]
135
134
136
- # Handle array constraints
137
- if field_schema .get ("type" ) == "array" :
138
- if "minItems" in field_schema :
139
- field_kwargs ["min_length" ] = field_schema ["minItems" ]
140
- if "maxItems" in field_schema :
141
- field_kwargs ["max_length" ] = field_schema ["maxItems" ]
135
+ field_info = Field (** field_kwargs ) if field_kwargs else Field ()
142
136
143
- if field_kwargs :
144
- return field_type , Field (** field_kwargs )
145
- else :
146
- return field_type , Field () if is_required else Field (default = None )
137
+ return field_type , field_info
147
138
148
139
@staticmethod
149
- def _get_python_type (field_schema : Dict [str , Any ]) -> Type :
140
+ def _get_python_type (schema : Dict [str , Any ]) -> Type [ Any ] :
150
141
"""Convert JSON schema type to Python type.
151
142
152
143
Args:
153
- field_schema : JSON schema for the field
144
+ schema : JSON schema dictionary
154
145
155
146
Returns:
156
147
Python type corresponding to the schema
157
148
"""
158
- schema_type = field_schema .get ("type" )
149
+ schema_type = schema .get ("type" )
159
150
160
151
if schema_type == "string" :
161
- if "enum" in field_schema :
162
- # Use Literal type instead of Enum for better string handling
163
- from typing import Literal
164
-
165
- enum_values = field_schema ["enum" ]
166
- if len (enum_values ) == 1 :
167
- return Literal [enum_values [0 ]]
168
- else :
169
- return Literal [tuple (enum_values )]
170
152
return str
171
-
172
153
elif schema_type == "integer" :
173
154
return int
174
-
175
155
elif schema_type == "number" :
176
156
return float
177
-
178
157
elif schema_type == "boolean" :
179
158
return bool
180
-
181
159
elif schema_type == "array" :
182
- items_schema = field_schema .get ("items" , {})
160
+ items_schema = schema .get ("items" , {})
183
161
if items_schema :
184
162
item_type = PydanticModelFactory ._get_python_type (items_schema )
185
- return List [item_type ]
186
- return List [ Any ]
187
-
163
+ return List [item_type ] # type: ignore[valid-type]
164
+ else :
165
+ return List [ Any ]
188
166
elif schema_type == "object" :
189
- # Handle nested objects
190
- if "properties" in field_schema :
191
- nested_model_name = f"NestedModel_{ abs (hash (str (field_schema )))} "
192
- return PydanticModelFactory .create_model_from_schema (nested_model_name , field_schema )
167
+ # For nested objects, we could recursively create models
168
+ # For now, return Dict[str, Any]
193
169
return Dict [str , Any ]
194
-
170
+ elif schema_type is None and "anyOf" in schema :
171
+ # Handle anyOf by creating Union types
172
+ types = []
173
+ for sub_schema in schema ["anyOf" ]:
174
+ sub_type = PydanticModelFactory ._get_python_type (sub_schema )
175
+ types .append (sub_type )
176
+ if len (types ) == 1 :
177
+ return types [0 ]
178
+ elif len (types ) == 2 and type (None ) in types :
179
+ # This is Optional[T]
180
+ non_none_type = next (t for t in types if t is not type (None ))
181
+ return Optional [non_none_type ] # type: ignore[return-value]
182
+ else :
183
+ return Union [tuple (types )] # type: ignore[return-value]
195
184
else :
196
- # Default to Any for unknown types
197
185
logger .warning ("Unknown schema type '%s', using Any" , schema_type )
198
186
return Any
199
187
200
188
@staticmethod
201
- def validate_schema ( schema : Dict [ str , Any ]) -> bool :
202
- """Validate that the schema is a valid JSON schema for object creation .
189
+ def get_schema_info ( model_class : Type [ BaseModel ]) -> Dict [ str , Any ] :
190
+ """Get schema information from a Pydantic model .
203
191
204
192
Args:
205
- schema: JSON schema dictionary
193
+ model_class: Pydantic model class
206
194
207
195
Returns:
208
- True if schema is valid, False otherwise
196
+ Dictionary containing schema information
209
197
"""
210
198
try :
211
- # Basic validation
212
- if not isinstance (schema , dict ):
213
- return False
214
-
215
- if schema .get ("type" ) != "object" :
216
- return False
217
-
218
- properties = schema .get ("properties" , {})
219
- if not isinstance (properties , dict ):
220
- return False
221
-
222
- # Validate each property
223
- for _prop_name , prop_schema in properties .items ():
224
- if not isinstance (prop_schema , dict ):
225
- return False
226
-
227
- if "type" not in prop_schema :
228
- return False
229
-
230
- return True
231
-
199
+ schema = model_class .model_json_schema ()
200
+ return {
201
+ "name" : model_class .__name__ ,
202
+ "schema" : schema ,
203
+ "fields" : list (schema .get ("properties" , {}).keys ()),
204
+ "required" : schema .get ("required" , []),
205
+ }
232
206
except Exception as e :
233
- logger .error ("Schema validation error: %s" , e )
234
- return False
235
-
236
- @staticmethod
237
- def get_schema_info (schema : Dict [str , Any ]) -> Dict [str , Any ]:
238
- """Extract information about a schema for debugging/documentation.
239
-
240
- Args:
241
- schema: JSON schema dictionary
242
-
243
- Returns:
244
- Dictionary with schema information
245
- """
246
- info = {
247
- "type" : schema .get ("type" ),
248
- "properties_count" : len (schema .get ("properties" , {})),
249
- "required_fields" : schema .get ("required" , []),
250
- "has_nested_objects" : False ,
251
- "has_arrays" : False ,
252
- "has_enums" : False ,
253
- }
254
-
255
- properties = schema .get ("properties" , {})
256
- for prop_schema in properties .values ():
257
- if prop_schema .get ("type" ) == "object" :
258
- info ["has_nested_objects" ] = True
259
- elif prop_schema .get ("type" ) == "array" :
260
- info ["has_arrays" ] = True
261
- elif "enum" in prop_schema :
262
- info ["has_enums" ] = True
263
-
264
- return info
207
+ logger .error ("Failed to get schema info for model '%s': %s" , model_class .__name__ , e )
208
+ return {
209
+ "name" : model_class .__name__ ,
210
+ "schema" : {},
211
+ "fields" : [],
212
+ "required" : [],
213
+ "error" : str (e ),
214
+ }
0 commit comments