string-schema 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,703 @@
1
+ """
2
+ String-based schema parsing for String Schema
3
+
4
+ Contains functionality for parsing human-readable schema strings into JSON Schema.
5
+ """
6
+
7
+ import re
8
+ from typing import Dict, Any, List, Union, Optional
9
+ import logging
10
+
11
+ from ..core.fields import SimpleField
12
+ from ..core.builders import simple_schema, list_of_objects_schema
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def parse_string_schema(schema_str: str, is_list: bool = False) -> Dict[str, Any]:
18
+ """Parse enhanced string schema with support for arrays, enums, unions, and special types"""
19
+ schema_str = schema_str.strip()
20
+ parsed_structure = _parse_schema_structure(schema_str)
21
+ return _structure_to_json_schema(parsed_structure)
22
+
23
+
24
+ def _parse_schema_structure(schema_str: str) -> Dict[str, Any]:
25
+ """Parse the overall structure of the schema with enhanced syntax support"""
26
+ schema_str = schema_str.strip()
27
+
28
+ # Handle curly brace syntax: {field1, field2}
29
+ if schema_str.startswith('{') and schema_str.endswith('}'):
30
+ inner_content = schema_str[1:-1].strip()
31
+ fields = _parse_object_fields(inner_content)
32
+ return {
33
+ "type": "object",
34
+ "fields": fields
35
+ }
36
+
37
+ # Handle array syntax: [type] or [{field1, field2}] with optional constraints
38
+ elif schema_str.startswith('['):
39
+ # Check for array constraints like [string](max=5)
40
+ array_constraints = {}
41
+
42
+ # Extract constraints if present: [type](constraints)
43
+ constraint_match = re.match(r'^\[([^\]]+)\]\(([^)]+)\)$', schema_str)
44
+ if constraint_match:
45
+ inner_content = constraint_match.group(1).strip()
46
+ constraint_str = constraint_match.group(2).strip()
47
+ array_constraints = _parse_array_constraints(constraint_str)
48
+ elif schema_str.endswith(']'):
49
+ # Simple array without constraints
50
+ inner_content = schema_str[1:-1].strip()
51
+ else:
52
+ # Malformed array syntax
53
+ return {"type": "object", "fields": {}} # Fallback
54
+
55
+ # Array of objects: [{field1, field2}]
56
+ if inner_content.startswith('{') and inner_content.endswith('}'):
57
+ object_fields = _parse_object_fields(inner_content[1:-1].strip())
58
+ return {
59
+ "type": "array",
60
+ "items": {
61
+ "type": "object",
62
+ "fields": object_fields
63
+ },
64
+ "constraints": array_constraints
65
+ }
66
+
67
+ # Array of simple types: [string], [int], [email], etc.
68
+ elif _is_simple_type(inner_content):
69
+ original_type = inner_content.strip().lower()
70
+ normalized_type = _normalize_type_name(original_type)
71
+ return {
72
+ "type": "array",
73
+ "items": {
74
+ "type": "simple",
75
+ "simple_type": normalized_type,
76
+ "original_type": original_type # Keep original for format hints
77
+ },
78
+ "constraints": array_constraints
79
+ }
80
+
81
+ # Array of complex fields: [name:string, age:int]
82
+ else:
83
+ fields = _parse_object_fields(inner_content)
84
+ return {
85
+ "type": "array",
86
+ "items": {
87
+ "type": "object",
88
+ "fields": fields
89
+ },
90
+ "constraints": array_constraints
91
+ }
92
+
93
+ # Simple object fields: name, age:int, email
94
+ else:
95
+ fields = _parse_object_fields(schema_str)
96
+ return {
97
+ "type": "object",
98
+ "fields": fields
99
+ }
100
+
101
+
102
+ def _parse_array_constraints(constraint_str: str) -> Dict[str, Any]:
103
+ """Parse array constraints like 'min=1,max=5'"""
104
+ constraints = {}
105
+ parts = [part.strip() for part in constraint_str.split(',')]
106
+
107
+ for part in parts:
108
+ if '=' in part:
109
+ key, value = part.split('=', 1)
110
+ key = key.strip()
111
+ value = value.strip()
112
+
113
+ try:
114
+ if key in ['min', 'max']:
115
+ constraints[key] = int(value)
116
+ else:
117
+ constraints[key] = value
118
+ except ValueError:
119
+ logger.warning(f"Invalid array constraint: {part}")
120
+
121
+ return constraints
122
+
123
+
124
+ def _parse_object_fields(fields_str: str) -> Dict[str, Any]:
125
+ """Parse object fields with enhanced syntax"""
126
+ fields_str = _normalize_string_schema(fields_str)
127
+ field_parts = _split_field_definitions_with_nesting(fields_str)
128
+
129
+ fields = {}
130
+ for field_part in field_parts:
131
+ field_name, field_def = _parse_single_field_with_nesting(field_part.strip())
132
+ if field_name:
133
+ fields[field_name] = field_def
134
+
135
+ return fields
136
+
137
+
138
+ def _parse_single_field_with_nesting(field_str: str) -> tuple:
139
+ """Parse a single field with enhanced syntax support"""
140
+ if not field_str:
141
+ return None, None
142
+
143
+ # Check for optional marker
144
+ required = True
145
+ if field_str.endswith('?'):
146
+ required = False
147
+ field_str = field_str[:-1].strip()
148
+
149
+ # Split field name and definition
150
+ if ':' in field_str:
151
+ field_name, field_def = field_str.split(':', 1)
152
+ field_name = field_name.strip()
153
+ field_def = field_def.strip()
154
+
155
+ # Handle nested structures
156
+ if field_def.startswith('[') or field_def.startswith('{'):
157
+ nested_structure = _parse_schema_structure(field_def)
158
+ nested_structure['required'] = required
159
+ return field_name, nested_structure
160
+
161
+ # Handle union types: string|int|null
162
+ elif '|' in field_def:
163
+ union_types = [t.strip() for t in field_def.split('|')]
164
+ field_type = _normalize_type_name(union_types[0]) # Use first type as primary
165
+
166
+ # Create field with union support
167
+ field_obj = SimpleField(
168
+ field_type=field_type,
169
+ required=required
170
+ )
171
+ # Store union info for JSON schema generation
172
+ field_obj.union_types = [_normalize_type_name(t) for t in union_types]
173
+ return field_name, field_obj
174
+
175
+ # Handle enum types: enum(value1,value2,value3) or choice(...)
176
+ elif field_def.startswith(('enum(', 'choice(', 'select(')):
177
+ enum_values = _parse_enum_values(field_def)
178
+ field_obj = SimpleField(
179
+ field_type="string", # Enums are string-based
180
+ required=required,
181
+ choices=enum_values
182
+ )
183
+ return field_name, field_obj
184
+
185
+ # Handle array types: array(string,max=5) or list(int,min=1)
186
+ elif field_def.startswith(('array(', 'list(')):
187
+ array_type, constraints = _parse_array_type_definition(field_def)
188
+ # Return as nested array structure
189
+ return field_name, {
190
+ "type": "array",
191
+ "items": {
192
+ "type": "simple",
193
+ "simple_type": array_type
194
+ },
195
+ "constraints": constraints,
196
+ "required": required
197
+ }
198
+
199
+ # Handle regular type with constraints
200
+ else:
201
+ original_type = field_def.split('(')[0].strip() # Get type before any constraints
202
+ field_type, constraints = _parse_type_definition(field_def)
203
+
204
+ # Add format hint for special types
205
+ if original_type in ['email', 'url', 'uri', 'datetime', 'date', 'uuid', 'phone']:
206
+ constraints['format_hint'] = original_type
207
+ field_type = 'string' # All special types are strings with format hints
208
+
209
+ field_obj = SimpleField(
210
+ field_type=field_type,
211
+ required=required,
212
+ **constraints
213
+ )
214
+ return field_name, field_obj
215
+
216
+ # Field name only (default to string)
217
+ else:
218
+ field_name = field_str.strip()
219
+ field_obj = SimpleField(
220
+ field_type="string",
221
+ required=required
222
+ )
223
+ return field_name, field_obj
224
+
225
+
226
+ def _parse_enum_values(enum_def: str) -> List[str]:
227
+ """Parse enum values from enum(value1,value2,value3)"""
228
+ # Extract content between parentheses
229
+ match = re.match(r'^(?:enum|choice|select)\(([^)]+)\)$', enum_def)
230
+ if not match:
231
+ return []
232
+
233
+ values_str = match.group(1)
234
+ values = [v.strip() for v in values_str.split(',')]
235
+ return values
236
+
237
+
238
+ def _parse_array_type_definition(array_def: str) -> tuple:
239
+ """Parse array(type,constraints) or list(type,constraints)"""
240
+ # Extract content between parentheses
241
+ match = re.match(r'^(?:array|list)\(([^)]+)\)$', array_def)
242
+ if not match:
243
+ return "string", {}
244
+
245
+ content = match.group(1)
246
+ parts = [p.strip() for p in content.split(',')]
247
+
248
+ # First part is the type
249
+ array_type = _normalize_type_name(parts[0]) if parts else "string"
250
+
251
+ # Remaining parts are constraints
252
+ constraints = {}
253
+ for part in parts[1:]:
254
+ if '=' in part:
255
+ key, value = part.split('=', 1)
256
+ key = key.strip()
257
+ value = value.strip()
258
+
259
+ try:
260
+ if key in ['min', 'max']:
261
+ constraints[key] = int(value)
262
+ else:
263
+ constraints[key] = value
264
+ except ValueError:
265
+ logger.warning(f"Invalid array constraint: {part}")
266
+
267
+ return array_type, constraints
268
+
269
+
270
+ def _is_simple_type(type_str: str) -> bool:
271
+ """Check if string represents a simple type"""
272
+ simple_types = [
273
+ 'string', 'str', 'text', 'int', 'integer', 'number', 'float', 'decimal',
274
+ 'bool', 'boolean', 'email', 'url', 'uri', 'datetime', 'date', 'uuid', 'phone'
275
+ ]
276
+ return type_str.strip().lower() in simple_types
277
+
278
+
279
+ def _structure_to_json_schema(structure: Dict[str, Any]) -> Dict[str, Any]:
280
+ """Convert parsed structure to JSON Schema"""
281
+ if structure["type"] == "object":
282
+ return _object_structure_to_schema(structure)
283
+ elif structure["type"] == "array":
284
+ return _array_structure_to_schema(structure)
285
+ else:
286
+ raise ValueError(f"Unknown structure type: {structure['type']}")
287
+
288
+
289
+ def _object_structure_to_schema(structure: Dict[str, Any]) -> Dict[str, Any]:
290
+ """Convert object structure to JSON Schema"""
291
+ properties = {}
292
+ required = []
293
+
294
+ for field_name, field_def in structure["fields"].items():
295
+ if isinstance(field_def, SimpleField):
296
+ prop_schema = _simple_field_to_json_schema(field_def)
297
+ properties[field_name] = prop_schema
298
+ if field_def.required:
299
+ required.append(field_name)
300
+ else:
301
+ # Nested structure
302
+ prop_schema = _structure_to_json_schema(field_def)
303
+ properties[field_name] = prop_schema
304
+ if field_def.get('required', True):
305
+ required.append(field_name)
306
+
307
+ schema = {
308
+ "type": "object",
309
+ "properties": properties
310
+ }
311
+ if required:
312
+ schema["required"] = required
313
+
314
+ return schema
315
+
316
+
317
+ def _array_structure_to_schema(structure: Dict[str, Any]) -> Dict[str, Any]:
318
+ """Convert array structure to JSON Schema"""
319
+ items_structure = structure["items"]
320
+ constraints = structure.get("constraints", {})
321
+
322
+ # Simple array: [string], [int], etc.
323
+ if items_structure["type"] == "simple":
324
+ simple_type = items_structure["simple_type"]
325
+ original_type = items_structure.get("original_type", simple_type)
326
+ items_schema = {"type": simple_type}
327
+
328
+ # Add format for special types
329
+ if simple_type == "string" and original_type in ['email', 'url', 'uri', 'datetime', 'date', 'uuid']:
330
+ if original_type == "email":
331
+ items_schema["format"] = "email"
332
+ elif original_type in ["url", "uri"]:
333
+ items_schema["format"] = "uri"
334
+ elif original_type == "datetime":
335
+ items_schema["format"] = "date-time"
336
+ elif original_type == "date":
337
+ items_schema["format"] = "date"
338
+ elif original_type == "uuid":
339
+ items_schema["format"] = "uuid"
340
+
341
+ else:
342
+ # Complex array: [{field1, field2}]
343
+ items_schema = _structure_to_json_schema(items_structure)
344
+
345
+ array_schema = {
346
+ "type": "array",
347
+ "items": items_schema
348
+ }
349
+
350
+ # Add array constraints
351
+ if "min" in constraints:
352
+ array_schema["minItems"] = constraints["min"]
353
+ if "max" in constraints:
354
+ array_schema["maxItems"] = constraints["max"]
355
+
356
+ return array_schema
357
+
358
+
359
+ def _simple_field_to_json_schema(field: SimpleField) -> Dict[str, Any]:
360
+ """Convert SimpleField to JSON Schema property with enhanced features"""
361
+ prop = {"type": field.field_type}
362
+
363
+ # Basic metadata
364
+ if field.description:
365
+ prop["description"] = field.description
366
+ if field.default is not None:
367
+ prop["default"] = field.default
368
+
369
+ # Handle union types
370
+ if hasattr(field, 'union_types') and field.union_types and len(field.union_types) > 1:
371
+ # Create anyOf for union types
372
+ union_schemas = []
373
+ for union_type in field.union_types:
374
+ if union_type == "null":
375
+ union_schemas.append({"type": "null"})
376
+ else:
377
+ union_schemas.append({"type": union_type})
378
+ prop = {"anyOf": union_schemas}
379
+
380
+ # Handle enum/choices
381
+ if field.choices:
382
+ prop["enum"] = field.choices
383
+
384
+ # Add format hints for special types
385
+ if hasattr(field, 'format_hint') and field.format_hint:
386
+ if field.format_hint == "email":
387
+ prop["format"] = "email"
388
+ elif field.format_hint in ["url", "uri"]:
389
+ prop["format"] = "uri"
390
+ elif field.format_hint == "datetime":
391
+ prop["format"] = "date-time"
392
+ elif field.format_hint == "date":
393
+ prop["format"] = "date"
394
+ elif field.format_hint == "uuid":
395
+ prop["format"] = "uuid"
396
+ # Note: phone doesn't have a standard JSON Schema format
397
+
398
+ # Numeric constraints
399
+ if field.field_type in ["integer", "number"]:
400
+ if field.min_val is not None:
401
+ prop["minimum"] = field.min_val
402
+ if field.max_val is not None:
403
+ prop["maximum"] = field.max_val
404
+
405
+ # String constraints
406
+ if field.field_type == "string":
407
+ if field.min_length is not None:
408
+ prop["minLength"] = field.min_length
409
+ if field.max_length is not None:
410
+ prop["maxLength"] = field.max_length
411
+
412
+ return prop
413
+
414
+
415
+ def _split_field_definitions_with_nesting(schema_str: str) -> List[str]:
416
+ """Split field definitions while respecting nesting"""
417
+ parts = []
418
+ current_part = ""
419
+ bracket_depth = 0
420
+ brace_depth = 0
421
+ paren_depth = 0
422
+
423
+ for char in schema_str:
424
+ if char == '[':
425
+ bracket_depth += 1
426
+ elif char == ']':
427
+ bracket_depth -= 1
428
+ elif char == '{':
429
+ brace_depth += 1
430
+ elif char == '}':
431
+ brace_depth -= 1
432
+ elif char == '(':
433
+ paren_depth += 1
434
+ elif char == ')':
435
+ paren_depth -= 1
436
+ elif char == ',' and bracket_depth == 0 and brace_depth == 0 and paren_depth == 0:
437
+ if current_part.strip():
438
+ parts.append(current_part.strip())
439
+ current_part = ""
440
+ continue
441
+
442
+ current_part += char
443
+
444
+ if current_part.strip():
445
+ parts.append(current_part.strip())
446
+
447
+ return parts
448
+
449
+
450
+ def _normalize_string_schema(schema_str: str) -> str:
451
+ """Normalize string schema by removing comments and extra whitespace"""
452
+ # Remove triple quotes
453
+ schema_str = re.sub(r'^[\'\"]{3}|[\'\"]{3}$', '', schema_str.strip())
454
+
455
+ # Process line by line
456
+ lines = schema_str.split('\n')
457
+ normalized_parts = []
458
+
459
+ for line in lines:
460
+ line = line.strip()
461
+ if not line or line.startswith('#'):
462
+ continue
463
+
464
+ # Remove inline comments
465
+ if ' #' in line:
466
+ line = line.split(' #')[0].strip()
467
+ elif '#' in line and not line.startswith('#'):
468
+ line = line.split('#')[0].strip()
469
+
470
+ normalized_parts.append(line)
471
+
472
+ # Join with commas and clean up
473
+ result = ', '.join(part.rstrip(',') for part in normalized_parts)
474
+ return result
475
+
476
+
477
+ def _parse_type_definition(type_def: str) -> tuple:
478
+ """Parse enhanced type definitions with constraints"""
479
+ constraints = {}
480
+
481
+ # Handle constraints in parentheses: type(min=1,max=10)
482
+ constraint_match = re.match(r'^(\w+)\(([^)]+)\)$', type_def)
483
+ if constraint_match:
484
+ base_type = constraint_match.group(1)
485
+ constraint_str = constraint_match.group(2)
486
+
487
+ # Parse constraints
488
+ constraint_parts = [part.strip() for part in constraint_str.split(',')]
489
+
490
+ # Handle positional constraints like int(0,120) -> min=0, max=120
491
+ if len(constraint_parts) == 2 and all('=' not in part for part in constraint_parts):
492
+ try:
493
+ min_val = float(constraint_parts[0]) if '.' in constraint_parts[0] else int(constraint_parts[0])
494
+ max_val = float(constraint_parts[1]) if '.' in constraint_parts[1] else int(constraint_parts[1])
495
+
496
+ if base_type in ['string', 'str', 'text']:
497
+ constraints['min_length'] = min_val
498
+ constraints['max_length'] = max_val
499
+ else:
500
+ constraints['min_val'] = min_val
501
+ constraints['max_val'] = max_val
502
+ except ValueError as e:
503
+ logger.warning(f"Invalid positional constraints '{constraint_str}': {e}")
504
+ else:
505
+ # Handle named constraints like string(min=1,max=100)
506
+ for part in constraint_parts:
507
+ if '=' in part:
508
+ key, value = part.split('=', 1)
509
+ key = key.strip()
510
+ value = value.strip()
511
+
512
+ try:
513
+ if key in ['min', 'max']:
514
+ if base_type in ['string', 'str', 'text']:
515
+ constraints[f'{key}_length'] = int(value)
516
+ else:
517
+ constraints[f'{key}_val'] = float(value) if '.' in value else int(value)
518
+ else:
519
+ constraints[key] = value
520
+ except ValueError as e:
521
+ logger.warning(f"Invalid constraint '{part}': {e}")
522
+ else:
523
+ # Single value constraint (treat as max)
524
+ try:
525
+ if base_type in ['string', 'str', 'text']:
526
+ constraints['max_length'] = int(part)
527
+ else:
528
+ constraints['max_val'] = float(part) if '.' in part else int(part)
529
+ except ValueError as e:
530
+ logger.warning(f"Invalid constraint value '{part}': {e}")
531
+ else:
532
+ base_type = type_def
533
+
534
+ # Normalize type name
535
+ normalized_type = _normalize_type_name(base_type)
536
+
537
+ return normalized_type, constraints
538
+
539
+
540
+ def _normalize_type_name(type_name: str) -> str:
541
+ """Normalize type names with enhanced support"""
542
+ type_mapping = {
543
+ # Basic types
544
+ 'str': 'string',
545
+ 'string': 'string',
546
+ 'text': 'string',
547
+ 'int': 'integer',
548
+ 'integer': 'integer',
549
+ 'num': 'number',
550
+ 'number': 'number',
551
+ 'float': 'number',
552
+ 'double': 'number',
553
+ 'decimal': 'number',
554
+ 'bool': 'boolean',
555
+ 'boolean': 'boolean',
556
+
557
+ # Special types (normalized to string, format handled separately)
558
+ 'email': 'string',
559
+ 'url': 'string',
560
+ 'uri': 'string',
561
+ 'datetime': 'string',
562
+ 'date': 'string',
563
+ 'uuid': 'string',
564
+ 'phone': 'string',
565
+ 'tel': 'string',
566
+ 'null': 'null',
567
+ }
568
+
569
+ return type_mapping.get(type_name.lower(), 'string')
570
+
571
+
572
+ # Clear function name aliases
573
+ def string_to_json_schema(schema_str: str) -> Dict[str, Any]:
574
+ """
575
+ Convert string syntax to JSON Schema dictionary.
576
+
577
+ Args:
578
+ schema_str: String schema definition (e.g., "name:string, email:email")
579
+
580
+ Returns:
581
+ JSON Schema dictionary
582
+
583
+ Example:
584
+ schema = string_to_json_schema("name:string, email:email")
585
+ """
586
+ return parse_string_schema(schema_str)
587
+
588
+
589
+ def validate_string_syntax(schema_str: str) -> Dict[str, Any]:
590
+ """
591
+ Validate string schema syntax and return detailed feedback.
592
+
593
+ Args:
594
+ schema_str: String schema definition to validate
595
+
596
+ Returns:
597
+ Dictionary with validation results: valid, errors, warnings, features_used, etc.
598
+
599
+ Example:
600
+ result = validate_string_syntax("name:string, email:email")
601
+ print(f"Valid: {result['valid']}")
602
+ """
603
+ return validate_string_schema(schema_str)
604
+
605
+
606
+ def validate_string_schema(schema_str: str) -> Dict[str, Any]:
607
+ """Validate enhanced string schema with detailed feedback"""
608
+ result = {
609
+ 'valid': False,
610
+ 'errors': [],
611
+ 'warnings': [],
612
+ 'parsed_fields': {},
613
+ 'generated_schema': None,
614
+ 'features_used': []
615
+ }
616
+
617
+ try:
618
+ # Parse the schema
619
+ schema = parse_string_schema(schema_str)
620
+ result['generated_schema'] = schema
621
+ result['valid'] = True
622
+
623
+ # Analyze features used
624
+ features = set()
625
+ if '[' in schema_str and ']' in schema_str:
626
+ features.add('arrays')
627
+ if '{' in schema_str and '}' in schema_str:
628
+ features.add('objects')
629
+ if '?' in schema_str:
630
+ features.add('optional_fields')
631
+ if 'enum(' in schema_str or 'choice(' in schema_str or 'select(' in schema_str:
632
+ features.add('enums')
633
+ if '|' in schema_str:
634
+ features.add('union_types')
635
+ if any(t in schema_str for t in ['email', 'url', 'datetime', 'date', 'uuid', 'phone']):
636
+ features.add('special_types')
637
+ # Check for constraints more carefully
638
+ constraint_patterns = [
639
+ r'string\([^)]+\)', # string(min=1,max=100)
640
+ r'int\([^)]+\)', # int(0,120)
641
+ r'number\([^)]+\)', # number(min=0)
642
+ r'text\([^)]+\)', # text(max=500)
643
+ r'\]\([^)]+\)' # [string](max=5)
644
+ ]
645
+ if any(re.search(pattern, schema_str) for pattern in constraint_patterns):
646
+ features.add('constraints')
647
+
648
+ result['features_used'] = list(features)
649
+
650
+ # Extract field information
651
+ _extract_field_info(schema, result['parsed_fields'], "")
652
+
653
+ # Add warnings for potential issues
654
+ if len(result['parsed_fields']) > 20:
655
+ result['warnings'].append("Schema has many fields (>20), consider simplifying")
656
+
657
+ if 'arrays' in features and 'constraints' not in features:
658
+ result['warnings'].append("Consider adding array size constraints for better LLM guidance")
659
+
660
+ except Exception as e:
661
+ result['errors'].append(str(e))
662
+
663
+ return result
664
+
665
+
666
+ def _extract_field_info(schema: Dict[str, Any], fields_dict: Dict[str, Any], prefix: str = ""):
667
+ """Extract field information from generated schema"""
668
+ if schema.get('type') == 'object' and 'properties' in schema:
669
+ required_fields = set(schema.get('required', []))
670
+
671
+ for field_name, field_schema in schema['properties'].items():
672
+ full_name = f"{prefix}.{field_name}" if prefix else field_name
673
+
674
+ field_info = {
675
+ 'type': field_schema.get('type', 'unknown'),
676
+ 'required': field_name in required_fields,
677
+ 'constraints': {}
678
+ }
679
+
680
+ # Extract constraints
681
+ for key in ['minimum', 'maximum', 'minLength', 'maxLength', 'minItems', 'maxItems', 'format', 'enum']:
682
+ if key in field_schema:
683
+ field_info['constraints'][key] = field_schema[key]
684
+
685
+ # Handle union types
686
+ if 'anyOf' in field_schema:
687
+ field_info['type'] = 'union'
688
+ field_info['union_types'] = [item.get('type', 'unknown') for item in field_schema['anyOf']]
689
+
690
+ fields_dict[full_name] = field_info
691
+
692
+ # Recurse into nested objects
693
+ if field_schema.get('type') == 'object':
694
+ _extract_field_info(field_schema, fields_dict, full_name)
695
+ elif field_schema.get('type') == 'array' and 'items' in field_schema:
696
+ items_schema = field_schema['items']
697
+ if items_schema.get('type') == 'object':
698
+ _extract_field_info(items_schema, fields_dict, f"{full_name}[]")
699
+
700
+ elif schema.get('type') == 'array' and 'items' in schema:
701
+ items_schema = schema['items']
702
+ if items_schema.get('type') == 'object':
703
+ _extract_field_info(items_schema, fields_dict, f"{prefix}[]" if prefix else "[]")