structurize 2.20.2__py3-none-any.whl → 2.20.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.
@@ -388,10 +388,10 @@ class JsonToStructureConverter:
388
388
  structure_type = 'string'
389
389
 
390
390
  elif json_primitive == 'integer':
391
- structure_type = self.detect_numeric_type({'type': 'integer', 'format': format, **schema})
391
+ structure_type = self.detect_numeric_type({**schema, 'type': 'integer', 'format': format})
392
392
 
393
393
  elif json_primitive == 'number':
394
- structure_type = self.detect_numeric_type({'type': 'number', 'format': format, **schema})
394
+ structure_type = self.detect_numeric_type({**schema, 'type': 'number', 'format': format})
395
395
 
396
396
  elif json_primitive == 'boolean':
397
397
  structure_type = 'boolean'
@@ -773,21 +773,38 @@ class JsonToStructureConverter:
773
773
  Returns:
774
774
  dict: JSON Structure choice definition
775
775
  """
776
+ # Handle both 'property' and 'propertyName' keys for compatibility
777
+ discriminator_property = discriminator_info.get('property') or discriminator_info.get('propertyName')
778
+ mapping = discriminator_info.get('mapping', {})
779
+
776
780
  choice_obj = {
777
781
  'type': 'choice',
778
- 'discriminator': discriminator_info['property'],
782
+ 'selector': discriminator_property,
779
783
  'choices': {}
780
784
  }
781
785
 
782
786
  if record_name:
783
787
  choice_obj['name'] = avro_name(record_name)
788
+
789
+ # Build reverse mapping from $ref to choice key
790
+ ref_to_key = {}
791
+ for key, ref in mapping.items():
792
+ ref_to_key[ref] = key
784
793
 
785
794
  # Process each choice option
786
795
  for i, option in enumerate(oneof_options):
787
796
  if '$ref' in option:
788
- # Handle reference
789
- choice_key = f"option_{i}" # Default key, ideally extract from mapping
790
- choice_obj['choices'][choice_key] = {'$ref': option['$ref']}
797
+ # Handle reference - use mapping to get the choice key
798
+ ref = option['$ref']
799
+ if ref in ref_to_key:
800
+ choice_key = ref_to_key[ref]
801
+ else:
802
+ # Extract name from reference
803
+ if ref.startswith('#/definitions/'):
804
+ choice_key = ref[14:] # Remove '#/definitions/' prefix
805
+ else:
806
+ choice_key = f"option_{i}"
807
+ choice_obj['choices'][choice_key] = {'$ref': ref}
791
808
  else:
792
809
  # Convert option to structure type
793
810
  choice_key = f"option_{i}"
@@ -908,6 +925,77 @@ class JsonToStructureConverter:
908
925
  'items': items_type
909
926
  }
910
927
 
928
+ def _process_array_type(self, json_type: dict, record_name: str, field_name: str, namespace: str,
929
+ dependencies: list, json_schema: dict, base_uri: str,
930
+ structure_schema: dict, record_stack: list, recursion_depth: int) -> dict:
931
+ """
932
+ Process an array type schema into JSON Structure format.
933
+
934
+ Args:
935
+ json_type: The JSON Schema with type: "array"
936
+ record_name: Name of the containing record
937
+ field_name: Name of the field
938
+ namespace: Namespace
939
+ dependencies: Dependencies list
940
+ json_schema: Full JSON schema
941
+ base_uri: Base URI
942
+ structure_schema: Structure schema being built
943
+ record_stack: Record stack for recursion detection
944
+ recursion_depth: Current recursion depth
945
+
946
+ Returns:
947
+ dict: JSON Structure array definition
948
+ """
949
+ items_schema = json_type.get('items', {})
950
+ is_set = json_type.get('uniqueItems', False)
951
+ return self.create_structure_array_or_set(
952
+ items_schema, is_set, record_name, namespace, dependencies,
953
+ json_schema, base_uri, structure_schema, record_stack, recursion_depth + 1
954
+ )
955
+
956
+ def _process_object_type(self, json_type: dict, record_name: str, field_name: str, namespace: str,
957
+ dependencies: list, json_schema: dict, base_uri: str,
958
+ structure_schema: dict, record_stack: list, recursion_depth: int) -> dict:
959
+ """
960
+ Process an object type schema into JSON Structure format.
961
+
962
+ Args:
963
+ json_type: The JSON Schema with type: "object"
964
+ record_name: Name of the containing record
965
+ field_name: Name of the field
966
+ namespace: Namespace
967
+ dependencies: Dependencies list
968
+ json_schema: Full JSON schema
969
+ base_uri: Base URI
970
+ structure_schema: Structure schema being built
971
+ record_stack: Record stack for recursion detection
972
+ recursion_depth: Current recursion depth
973
+
974
+ Returns:
975
+ dict: JSON Structure object definition
976
+ """
977
+ properties = json_type.get('properties', {})
978
+ required = json_type.get('required', [])
979
+
980
+ structure_properties = {}
981
+ for prop_name, prop_schema in properties.items():
982
+ prop_type = self.json_type_to_structure_type(
983
+ prop_schema, record_name, prop_name, namespace, dependencies,
984
+ json_schema, base_uri, structure_schema, record_stack, recursion_depth + 1
985
+ )
986
+ prop_type = self._ensure_schema_object(prop_type, structure_schema, prop_name)
987
+ structure_properties[prop_name] = prop_type
988
+
989
+ result = {
990
+ 'type': 'object',
991
+ 'properties': structure_properties
992
+ }
993
+
994
+ if required:
995
+ result['required'] = required
996
+
997
+ return result
998
+
911
999
  def add_alternate_names(self, structure: dict, original_name: str) -> dict:
912
1000
  """
913
1001
  Add alternate names for different naming conventions.
@@ -1005,13 +1093,25 @@ class JsonToStructureConverter:
1005
1093
  """
1006
1094
  Flatten the list of types in a union into a single list.
1007
1095
 
1096
+ JSON Structure Core requires union members to be either:
1097
+ - Simple type strings (e.g., "string", "null", "boolean")
1098
+ - References to definitions (e.g., {"$ref": "#/definitions/Foo"})
1099
+
1100
+ Inline compound types or primitives with constraints must be hoisted to definitions.
1101
+
1008
1102
  Args:
1009
1103
  type_list (list): The list of types in a union.
1104
+ structure_schema: The structure schema for hoisting definitions.
1105
+ name_hint: Hint for naming hoisted definitions.
1010
1106
 
1011
1107
  Returns:
1012
1108
  list: The flattened list of types.
1013
1109
  """
1014
1110
  flat_list = []
1111
+ simple_primitives = {'string', 'boolean', 'integer', 'number', 'null', 'int8', 'int16',
1112
+ 'int32', 'int64', 'float', 'double', 'bytes', 'uuid', 'datetime',
1113
+ 'date', 'time', 'duration', 'decimal'}
1114
+
1015
1115
  for idx, t in enumerate(type_list):
1016
1116
  if isinstance(t, list):
1017
1117
  inner = self.flatten_union(t, structure_schema, name_hint)
@@ -1019,17 +1119,52 @@ class JsonToStructureConverter:
1019
1119
  obj = self._ensure_schema_object(u, structure_schema, f"{name_hint}_option_{idx}" if name_hint else None, force_hoist_in_union=True)
1020
1120
  if obj not in flat_list:
1021
1121
  flat_list.append(obj)
1022
- else:
1023
- # For primitive types in unions, extract the type string directly
1024
- if isinstance(t, dict) and 'type' in t and t['type'] in ['string', 'boolean', 'integer', 'number', 'null'] and len(t) == 1:
1025
- # This is a simple primitive type object like {"type": "boolean"} - extract the type string
1122
+ elif isinstance(t, str):
1123
+ # Simple type string - use directly
1124
+ if t not in flat_list:
1125
+ flat_list.append(t)
1126
+ elif isinstance(t, dict):
1127
+ # Check if it's a simple primitive (type only, no constraints)
1128
+ if '$ref' in t:
1129
+ # Reference - use directly
1130
+ if t not in flat_list:
1131
+ flat_list.append(t)
1132
+ elif 'type' in t and t['type'] in simple_primitives and len(t) == 1:
1133
+ # Simple primitive type object like {"type": "boolean"} - extract the type string
1026
1134
  if t['type'] not in flat_list:
1027
1135
  flat_list.append(t['type'])
1136
+ elif 'type' in t and t['type'] in simple_primitives:
1137
+ # Primitive with constraints (e.g., {type: "string", maxLength: 280})
1138
+ # This needs to be hoisted because inline constraints not allowed in union
1139
+ if structure_schema is not None:
1140
+ hoisted = self._hoist_definition(t, structure_schema, f"{name_hint or 'union'}_{t['type']}")
1141
+ if hoisted not in flat_list:
1142
+ flat_list.append(hoisted)
1143
+ else:
1144
+ # No structure schema for hoisting - fallback to just the type
1145
+ if t['type'] not in flat_list:
1146
+ flat_list.append(t['type'])
1147
+ elif 'type' in t and t['type'] in ('array', 'object', 'set', 'map', 'choice'):
1148
+ # Compound type - must be hoisted
1149
+ if structure_schema is not None:
1150
+ hoisted = self._hoist_definition(t, structure_schema, f"{name_hint or 'union'}_{t['type']}")
1151
+ if hoisted not in flat_list:
1152
+ flat_list.append(hoisted)
1153
+ else:
1154
+ # No structure schema - use as is (will likely fail validation)
1155
+ obj = self._ensure_schema_object(t, structure_schema, f"{name_hint}_option_{idx}" if name_hint else None, force_hoist_in_union=True)
1156
+ if obj not in flat_list:
1157
+ flat_list.append(obj)
1028
1158
  else:
1029
- # For complex types, use the normal processing
1159
+ # Other dict structure - use normal processing
1030
1160
  obj = self._ensure_schema_object(t, structure_schema, f"{name_hint}_option_{idx}" if name_hint else None, force_hoist_in_union=True)
1031
1161
  if obj not in flat_list:
1032
1162
  flat_list.append(obj)
1163
+ else:
1164
+ # Unknown type - use normal processing
1165
+ obj = self._ensure_schema_object(t, structure_schema, f"{name_hint}_option_{idx}" if name_hint else None, force_hoist_in_union=True)
1166
+ if obj not in flat_list:
1167
+ flat_list.append(obj)
1033
1168
  return flat_list
1034
1169
 
1035
1170
  def merge_structure_schemas(self, schemas: list, structure_schemas: list, type_name: str | None = None, deps: List[str] = []) -> str | list | dict:
@@ -1244,6 +1379,61 @@ class JsonToStructureConverter:
1244
1379
  'type': 'object',
1245
1380
  'properties': {} }
1246
1381
 
1382
+ # Handle type arrays (e.g., ["integer", "null"] for nullable in JSON Schema 2020-12/OpenAPI 3.1)
1383
+ if json_type.get('type') and isinstance(json_type['type'], list):
1384
+ type_list = json_type['type']
1385
+ format_hint = json_type.get('format')
1386
+ enum_values = json_type.get('enum')
1387
+
1388
+ # Check if any type in the list is a compound type (array, object)
1389
+ compound_types = {'array', 'object'}
1390
+ has_compound = any(t in compound_types for t in type_list if isinstance(t, str))
1391
+
1392
+ if has_compound:
1393
+ # For compound types, we need to process each variant separately
1394
+ # and create a proper union. Compound types must be hoisted to definitions
1395
+ # because JSON Structure Core doesn't allow inline compound types in unions.
1396
+ union_members = []
1397
+ for t in type_list:
1398
+ if t == 'null':
1399
+ union_members.append('null')
1400
+ elif t == 'array':
1401
+ # Process array with the full schema context (items, etc.)
1402
+ array_schema = {**json_type, 'type': 'array'}
1403
+ array_type = self._process_array_type(
1404
+ array_schema, record_name, field_name, namespace, dependencies,
1405
+ json_schema, base_uri, structure_schema, record_stack, recursion_depth
1406
+ )
1407
+ # Hoist to definitions - inline compound types not allowed in union
1408
+ hoisted = self._hoist_definition(array_type, structure_schema, f"{field_name or record_name}_array")
1409
+ union_members.append(hoisted)
1410
+ elif t == 'object':
1411
+ # Process object with the full schema context (properties, etc.)
1412
+ object_schema = {**json_type, 'type': 'object'}
1413
+ object_type = self._process_object_type(
1414
+ object_schema, record_name, field_name, namespace, dependencies,
1415
+ json_schema, base_uri, structure_schema, record_stack, recursion_depth
1416
+ )
1417
+ # Hoist to definitions - inline compound types not allowed in union
1418
+ hoisted = self._hoist_definition(object_type, structure_schema, f"{field_name or record_name}_object")
1419
+ union_members.append(hoisted)
1420
+ elif isinstance(t, str):
1421
+ # Primitive type
1422
+ prim_type = self.json_schema_primitive_to_structure_type(
1423
+ t, format_hint, enum_values, record_name, field_name, namespace, dependencies, json_type
1424
+ )
1425
+ union_members.append(prim_type)
1426
+
1427
+ return {'type': self.flatten_union(union_members, structure_schema, field_name)}
1428
+ else:
1429
+ # All primitives, use the existing logic
1430
+ structure_type = self.json_schema_primitive_to_structure_type(
1431
+ type_list, format_hint, enum_values, record_name, field_name, namespace, dependencies, json_type
1432
+ )
1433
+ if isinstance(structure_type, dict):
1434
+ structure_type = self.add_validation_constraints(structure_type, json_type)
1435
+ return structure_type
1436
+
1247
1437
  if json_type.get('type') and isinstance(json_type['type'], str):
1248
1438
  # Check if this schema also has composition keywords that should be preserved
1249
1439
  if self.preserve_composition and self.has_composition_keywords(json_type):
@@ -2160,12 +2350,20 @@ class JsonToStructureConverter:
2160
2350
  if 'allOf' in json_type:
2161
2351
  # Merge all schemas in allOf
2162
2352
  merged = {}
2353
+ has_ref = False
2354
+ ref_value = None
2355
+
2163
2356
  for schema in json_type['allOf']:
2164
2357
  converted = self.json_type_to_structure_type(
2165
2358
  schema, record_name, field_name, namespace, dependencies,
2166
2359
  json_schema, base_uri, structure_schema, record_stack, recursion_depth + 1
2167
2360
  )
2168
2361
  if isinstance(converted, dict):
2362
+ # Track if we have a $ref
2363
+ if '$ref' in converted:
2364
+ has_ref = True
2365
+ ref_value = converted['$ref']
2366
+
2169
2367
  # Simple merge - in real scenarios this would be more complex
2170
2368
  for key, value in converted.items():
2171
2369
  if key == 'properties' and key in merged:
@@ -2174,6 +2372,28 @@ class JsonToStructureConverter:
2174
2372
  merged[key] = list(set(merged[key] + value))
2175
2373
  else:
2176
2374
  merged[key] = value
2375
+
2376
+ # JSON Structure doesn't allow both $ref and type
2377
+ # If we have a $ref and type with no meaningful properties, just use $ref
2378
+ if has_ref and 'type' in merged:
2379
+ has_meaningful_properties = (
2380
+ 'properties' in merged and merged['properties'] or
2381
+ 'required' in merged and merged['required'] or
2382
+ 'items' in merged
2383
+ )
2384
+ if not has_meaningful_properties:
2385
+ # Just return the reference
2386
+ result = {'$ref': ref_value}
2387
+ # Copy metadata fields if present
2388
+ for meta_key in ['description', 'title', 'doc']:
2389
+ if meta_key in merged:
2390
+ result[meta_key] = merged[meta_key]
2391
+ return result
2392
+ else:
2393
+ # Has meaningful extensions - need to create a proper extension
2394
+ # Remove the $ref and keep the merged type
2395
+ del merged['$ref']
2396
+
2177
2397
  return merged
2178
2398
 
2179
2399
  elif 'anyOf' in json_type:
@@ -2186,8 +2406,10 @@ class JsonToStructureConverter:
2186
2406
  )
2187
2407
  anyof_schemas.append(converted)
2188
2408
 
2409
+ # Use flatten_union to properly hoist compound types
2410
+ flattened = self.flatten_union(anyof_schemas, structure_schema, field_name or record_name)
2189
2411
  return {
2190
- 'type': anyof_schemas
2412
+ 'type': flattened
2191
2413
  }
2192
2414
 
2193
2415
  elif 'oneOf' in json_type: