1010
1111JsonSchema = dict [str , Any ]
1212
13- __all__ = ['JsonSchemaTransformer' , 'InlineDefsJsonSchemaTransformer' , 'flatten_allof' ]
13+ __all__ = ['JsonSchemaTransformer' , 'InlineDefsJsonSchemaTransformer' ]
1414
1515
1616@dataclass (init = False )
@@ -81,7 +81,7 @@ def walk(self) -> JsonSchema:
8181 def _handle (self , schema : JsonSchema ) -> JsonSchema :
8282 # Flatten allOf if requested, before processing the schema
8383 if self .flatten_allof :
84- schema = flatten_allof (schema )
84+ schema = _recurse_flatten_allof (schema )
8585
8686 nested_refs = 0
8787 if self .prefer_inlined_defs :
@@ -209,9 +209,123 @@ def _allof_is_object_like(member: JsonSchema) -> bool:
209209
210210
211211def _merge_additional_properties_values (values : list [Any ]) -> bool | JsonSchema :
212+ # If any value is False, return False (most restrictive)
213+ if any (v is False for v in values ):
214+ return False
215+ # If any value is a dict schema, we can't easily merge multiple schemas, so return True
212216 if any (isinstance (v , dict ) for v in values ):
213217 return True
214- return False if values and all (v is False for v in values ) else True
218+ # Default to True (allow additional properties)
219+ return True
220+
221+
222+ def _collect_member_data (
223+ processed_members : list [JsonSchema ],
224+ ) -> tuple [
225+ dict [str , JsonSchema ],
226+ set [str ],
227+ dict [str , JsonSchema ],
228+ list [Any ],
229+ list [set [str ]],
230+ list [dict [str , JsonSchema ]],
231+ list [Any ],
232+ ]:
233+ """Collect properties, required, patternProperties, and additionalProperties from all members."""
234+ properties : dict [str , JsonSchema ] = {}
235+ required : set [str ] = set ()
236+ pattern_properties : dict [str , JsonSchema ] = {}
237+ additional_values : list [Any ] = []
238+ restricted_property_sets : list [set [str ]] = []
239+ members_properties : list [dict [str , JsonSchema ]] = []
240+ members_additional_props : list [Any ] = []
241+
242+ for m in processed_members :
243+ member_properties_raw = m .get ('properties' )
244+ member_properties : dict [str , JsonSchema ] = (
245+ cast (dict [str , JsonSchema ], member_properties_raw ) if isinstance (member_properties_raw , dict ) else {}
246+ )
247+ members_properties .append (member_properties )
248+ members_additional_props .append (m .get ('additionalProperties' ))
249+
250+ if member_properties :
251+ properties .update (member_properties )
252+ if isinstance (m .get ('required' ), list ):
253+ required .update (m ['required' ])
254+ if isinstance (m .get ('patternProperties' ), dict ):
255+ pattern_properties .update (m ['patternProperties' ])
256+ if 'additionalProperties' in m :
257+ additional_values .append (m ['additionalProperties' ])
258+ if m ['additionalProperties' ] is False :
259+ restricted_property_sets .append (set (member_properties .keys ()))
260+
261+ return (
262+ properties ,
263+ required ,
264+ pattern_properties ,
265+ additional_values ,
266+ restricted_property_sets ,
267+ members_properties ,
268+ members_additional_props ,
269+ )
270+
271+
272+ def _filter_by_restricted_property_sets (merged : JsonSchema , restricted_property_sets : list [set [str ]]) -> None :
273+ """Filter properties to only those allowed by all members with additionalProperties: False."""
274+ if not restricted_property_sets :
275+ return
276+
277+ allowed_names = restricted_property_sets [0 ].copy ()
278+ for prop_set in restricted_property_sets [1 :]:
279+ allowed_names &= prop_set
280+
281+ if 'properties' in merged :
282+ merged ['properties' ] = {k : v for k , v in merged ['properties' ].items () if k in allowed_names }
283+ if not merged ['properties' ]:
284+ merged .pop ('properties' )
285+ if 'required' in merged :
286+ merged ['required' ] = [k for k in merged ['required' ] if k in allowed_names ]
287+ if not merged ['required' ]:
288+ merged .pop ('required' )
289+
290+
291+ def _filter_incompatible_properties (
292+ merged : JsonSchema ,
293+ members_properties : list [dict [str , JsonSchema ]],
294+ members_additional_props : list [Any ],
295+ ) -> None :
296+ """Filter properties that are incompatible with additionalProperties constraints."""
297+ if 'properties' not in merged :
298+ return
299+
300+ incompatible_props : set [str ] = set ()
301+ for prop_name , prop_schema in merged ['properties' ].items ():
302+ prop_types = _get_type_set (prop_schema )
303+ for member_props , member_additional in zip (members_properties , members_additional_props ):
304+ if prop_name in member_props :
305+ member_prop_types = _get_type_set (member_props [prop_name ])
306+ if prop_types and member_prop_types and not prop_types & member_prop_types :
307+ incompatible_props .add (prop_name )
308+ break
309+ continue
310+
311+ if member_additional is False :
312+ incompatible_props .add (prop_name )
313+ break
314+
315+ if isinstance (member_additional , dict ):
316+ allowed_types = _get_type_set (cast (JsonSchema , member_additional ))
317+ if prop_types and allowed_types and not prop_types <= allowed_types :
318+ incompatible_props .add (prop_name )
319+ break
320+
321+ if incompatible_props :
322+ merged ['properties' ] = {k : v for k , v in merged ['properties' ].items () if k not in incompatible_props }
323+ if not merged ['properties' ]:
324+ merged .pop ('properties' )
325+ if 'required' in merged :
326+ merged ['required' ] = [k for k in merged ['required' ] if k not in incompatible_props ]
327+ if not merged ['required' ]:
328+ merged .pop ('required' )
215329
216330
217331def _flatten_current_level (s : JsonSchema ) -> JsonSchema :
@@ -230,37 +344,66 @@ def _flatten_current_level(s: JsonSchema) -> JsonSchema:
230344 merged : JsonSchema = {k : v for k , v in s .items () if k != 'allOf' }
231345 merged ['type' ] = 'object'
232346
347+ # Collect initial properties from merged schema
233348 properties : dict [str , JsonSchema ] = {}
234349 if isinstance (merged .get ('properties' ), dict ):
235350 properties .update (merged ['properties' ])
236351
237352 required : set [str ] = set (merged .get ('required' , []) or [])
238353 pattern_properties : dict [str , JsonSchema ] = dict (merged .get ('patternProperties' , {}) or {})
239- additional_values : list [Any ] = []
240-
241- for m in processed_members :
242- if isinstance (m .get ('properties' ), dict ):
243- properties .update (m ['properties' ])
244- if isinstance (m .get ('required' ), list ):
245- required .update (m ['required' ])
246- if isinstance (m .get ('patternProperties' ), dict ):
247- pattern_properties .update (m ['patternProperties' ])
248- if 'additionalProperties' in m :
249- additional_values .append (m ['additionalProperties' ])
250354
355+ # Collect data from all members
356+ (
357+ member_properties ,
358+ member_required ,
359+ member_pattern_properties ,
360+ additional_values ,
361+ restricted_property_sets ,
362+ members_properties ,
363+ members_additional_props ,
364+ ) = _collect_member_data (processed_members )
365+
366+ # Merge all collected data
367+ properties .update (member_properties )
368+ required .update (member_required )
369+ pattern_properties .update (member_pattern_properties )
370+
371+ # Apply merged properties, required, and patternProperties
251372 if properties :
252373 merged ['properties' ] = {k : _recurse_flatten_allof (v ) for k , v in properties .items ()}
253374 if required :
254375 merged ['required' ] = sorted (required )
255376 if pattern_properties :
256377 merged ['patternProperties' ] = {k : _recurse_flatten_allof (v ) for k , v in pattern_properties .items ()}
257378
379+ # Filter by restricted property sets (additionalProperties: False)
380+ _filter_by_restricted_property_sets (merged , restricted_property_sets )
381+
382+ # Merge additionalProperties
258383 if additional_values :
259384 merged ['additionalProperties' ] = _merge_additional_properties_values (additional_values )
260385
386+ # Filter incompatible properties based on additionalProperties constraints
387+ _filter_incompatible_properties (merged , members_properties , members_additional_props )
388+
261389 return merged
262390
263391
392+ def _get_type_set (schema : JsonSchema | None ) -> set [str ] | None :
393+ if not schema :
394+ return None
395+ schema_type = schema .get ('type' )
396+ if isinstance (schema_type , list ):
397+ result : set [str ] = set ()
398+ type_list : list [Any ] = cast (list [Any ], schema_type )
399+ for t in type_list :
400+ result .add (str (t ))
401+ return result
402+ if isinstance (schema_type , str ):
403+ return {schema_type }
404+ return None
405+
406+
264407def _recurse_children (s : JsonSchema ) -> JsonSchema :
265408 t = s .get ('type' )
266409 if t == 'object' :
@@ -292,14 +435,3 @@ def _recurse_flatten_allof(schema: JsonSchema) -> JsonSchema:
292435 s = _flatten_current_level (s )
293436 s = _recurse_children (s )
294437 return s
295-
296-
297- def flatten_allof (schema : JsonSchema ) -> JsonSchema :
298- """Flatten simple object-only allOf combinations by merging object members.
299-
300- - Merges properties and unions required lists.
301- - Combines additionalProperties conservatively: only False if all are False; otherwise True.
302- - Recurses into nested object/array members.
303- - Leaves non-object allOfs untouched.
304- """
305- return _recurse_flatten_allof (schema )
0 commit comments