2323from openapi_core .schema .protocols import SuportsGetAll
2424from openapi_core .schema .protocols import SuportsGetList
2525from openapi_core .schema .schemas import get_properties
26+ from openapi_core .validation .schemas .exceptions import ValidateError
2627from openapi_core .validation .schemas .validators import SchemaValidator
2728
2829if TYPE_CHECKING :
@@ -126,6 +127,8 @@ def evolve(
126127 schema = schema ,
127128 schema_validator = schema_validator ,
128129 schema_caster = schema_caster ,
130+ encoding = self .encoding ,
131+ ** self .parameters ,
129132 )
130133
131134 def decode (
@@ -137,27 +140,21 @@ def decode(
137140
138141 # For urlencoded/multipart, use caster for oneOf/anyOf detection if validator available
139142 if self .schema_validator is not None :
140- one_of_schema = self .schema_validator .get_one_of_schema (
141- location , caster = self .schema_caster
142- )
143+ one_of_schema = self .get_composed_one_of_schema (location )
143144 if one_of_schema is not None :
144145 one_of_properties = self .evolve (one_of_schema ).decode (
145146 location , schema_only = True
146147 )
147148 properties .update (one_of_properties )
148149
149- any_of_schemas = self .schema_validator .iter_any_of_schemas (
150- location , caster = self .schema_caster
151- )
150+ any_of_schemas = self .iter_composed_any_of_schemas (location )
152151 for any_of_schema in any_of_schemas :
153152 any_of_properties = self .evolve (any_of_schema ).decode (
154153 location , schema_only = True
155154 )
156155 properties .update (any_of_properties )
157156
158- all_of_schemas = self .schema_validator .iter_all_of_schemas (
159- location
160- )
157+ all_of_schemas = self .iter_composed_all_of_schemas (location )
161158 for all_of_schema in all_of_schemas :
162159 all_of_properties = self .evolve (all_of_schema ).decode (
163160 location , schema_only = True
@@ -220,6 +217,13 @@ def decode_property_style(
220217 location : Mapping [str , Any ],
221218 prep_encoding : SchemaPath ,
222219 ) -> Any :
220+ if self .mimetype .startswith ("multipart" ):
221+ location = self .normalize_multipart_form_location (
222+ prop_name ,
223+ prop_schema ,
224+ location ,
225+ )
226+
223227 prop_style , prop_explode = get_style_and_explode (
224228 prep_encoding , default_location = "query"
225229 )
@@ -228,6 +232,81 @@ def decode_property_style(
228232 )
229233 return prop_deserializer .deserialize (location )
230234
235+ def normalize_multipart_form_location (
236+ self ,
237+ prop_name : str ,
238+ prop_schema : SchemaPath ,
239+ location : Mapping [str , Any ],
240+ ) -> Mapping [str , Any ]:
241+ if not self .should_decode_multipart_form_value (prop_schema ):
242+ return location
243+
244+ if prop_name not in location :
245+ return location
246+
247+ normalized = dict (location )
248+ value = location [prop_name ]
249+
250+ if isinstance (value , bytes ):
251+ normalized [prop_name ] = self .decode_multipart_form_value (value )
252+ return normalized
253+
254+ if isinstance (value , list ):
255+ normalized [prop_name ] = [
256+ (
257+ self .decode_multipart_form_value (item )
258+ if isinstance (item , bytes )
259+ else item
260+ )
261+ for item in value
262+ ]
263+ return normalized
264+
265+ if isinstance (location , SuportsGetAll ):
266+ values = location .getall (prop_name )
267+ if any (isinstance (item , bytes ) for item in values ):
268+ normalized [prop_name ] = [
269+ (
270+ self .decode_multipart_form_value (item )
271+ if isinstance (item , bytes )
272+ else item
273+ )
274+ for item in values
275+ ]
276+ return normalized
277+
278+ if isinstance (location , SuportsGetList ):
279+ values = location .getlist (prop_name )
280+ if any (isinstance (item , bytes ) for item in values ):
281+ normalized [prop_name ] = [
282+ (
283+ self .decode_multipart_form_value (item )
284+ if isinstance (item , bytes )
285+ else item
286+ )
287+ for item in values
288+ ]
289+
290+ return normalized
291+
292+ def should_decode_multipart_form_value (
293+ self ,
294+ prop_schema : SchemaPath ,
295+ ) -> bool :
296+ schema_type = (prop_schema / "type" ).read_str (None )
297+ schema_format = (prop_schema / "format" ).read_str (None )
298+
299+ if schema_type in ["integer" , "number" , "boolean" ]:
300+ return True
301+
302+ return schema_type == "string" and schema_format != "binary"
303+
304+ def decode_multipart_form_value (self , value : bytes ) -> str :
305+ try :
306+ return value .decode ("utf-8" )
307+ except UnicodeDecodeError :
308+ return value .decode ("ASCII" , errors = "surrogateescape" )
309+
231310 def decode_property_content_type (
232311 self ,
233312 prop_name : str ,
@@ -253,3 +332,76 @@ def decode_property_content_type(
253332 return list (map (prop_deserializer .deserialize , value ))
254333
255334 return prop_deserializer .deserialize (location [prop_name ])
335+
336+ def get_composed_one_of_schema (
337+ self , location : Mapping [str , Any ]
338+ ) -> Optional [SchemaPath ]:
339+ assert self .schema_validator is not None
340+
341+ if not self .mimetype .startswith ("multipart" ):
342+ return self .schema_validator .get_one_of_schema (
343+ location , caster = self .schema_caster
344+ )
345+
346+ if self .schema is None or "oneOf" not in self .schema :
347+ return None
348+
349+ for subschema in self .schema / "oneOf" :
350+ if self .is_decoded_subschema_valid (subschema , location ):
351+ return subschema
352+
353+ return None
354+
355+ def iter_composed_any_of_schemas (
356+ self , location : Mapping [str , Any ]
357+ ) -> list [SchemaPath ]:
358+ assert self .schema_validator is not None
359+
360+ if not self .mimetype .startswith ("multipart" ):
361+ return list (
362+ self .schema_validator .iter_any_of_schemas (
363+ location , caster = self .schema_caster
364+ )
365+ )
366+
367+ if self .schema is None or "anyOf" not in self .schema :
368+ return []
369+
370+ return [
371+ subschema
372+ for subschema in self .schema / "anyOf"
373+ if self .is_decoded_subschema_valid (subschema , location )
374+ ]
375+
376+ def iter_composed_all_of_schemas (
377+ self , location : Mapping [str , Any ]
378+ ) -> list [SchemaPath ]:
379+ assert self .schema_validator is not None
380+
381+ if not self .mimetype .startswith ("multipart" ):
382+ return list (self .schema_validator .iter_all_of_schemas (location ))
383+
384+ if self .schema is None or "allOf" not in self .schema :
385+ return []
386+
387+ return [
388+ subschema
389+ for subschema in self .schema / "allOf"
390+ if self .is_decoded_subschema_valid (subschema , location )
391+ ]
392+
393+ def is_decoded_subschema_valid (
394+ self ,
395+ subschema : SchemaPath ,
396+ location : Mapping [str , Any ],
397+ ) -> bool :
398+ assert self .schema_validator is not None
399+
400+ deserializer = self .evolve (subschema )
401+ candidate = deserializer .decode (location )
402+ validator = self .schema_validator .evolve (subschema )
403+ try :
404+ validator .validate (candidate )
405+ except ValidateError :
406+ return False
407+ return True
0 commit comments