1+ from dataclasses import dataclass
12from typing import TYPE_CHECKING
23from typing import Any
4+ from typing import Iterator
35from typing import Mapping
46from typing import Optional
57from xml .etree .ElementTree import ParseError
68
79from jsonschema_path import SchemaPath
810
11+ from openapi_core .deserializing .exceptions import DeserializeError
912from openapi_core .deserializing .media_types .datatypes import (
1013 DeserializerCallable ,
1114)
2326from openapi_core .schema .protocols import SuportsGetAll
2427from openapi_core .schema .protocols import SuportsGetList
2528from openapi_core .schema .schemas import get_properties
29+ from openapi_core .validation .schemas .exceptions import ValidateError
2630from openapi_core .validation .schemas .validators import SchemaValidator
2731
2832if TYPE_CHECKING :
@@ -63,6 +67,12 @@ def get_deserializer_callable(
6367 return self .media_type_deserializers [mimetype ]
6468
6569
70+ @dataclass (frozen = True )
71+ class FormMediaSchemaMatch :
72+ schema : SchemaPath
73+ decoded_candidate : Mapping [str , Any ]
74+
75+
6676class MediaTypeDeserializer :
6777 def __init__ (
6878 self ,
@@ -97,7 +107,7 @@ def deserialize(self, value: bytes) -> Any:
97107 ):
98108 return deserialized
99109
100- # decode multipart request bodies if schema provided
110+ # Decode form-media bodies only when a schema is available.
101111 if self .schema is not None :
102112 return self .decode (deserialized )
103113
@@ -126,51 +136,41 @@ def evolve(
126136 schema = schema ,
127137 schema_validator = schema_validator ,
128138 schema_caster = schema_caster ,
139+ encoding = self .encoding ,
140+ ** self .parameters ,
129141 )
130142
131143 def decode (
132- self , location : Mapping [str , Any ], schema_only : bool = False
144+ self ,
145+ location : Mapping [str , Any ],
146+ schema_only : bool = False ,
147+ use_defaults : bool = True ,
133148 ) -> Mapping [str , Any ]:
134- # schema is required for multipart
149+ # Form-media decoding always needs a schema to resolve properties.
135150 assert self .schema is not None
136151 properties : dict [str , Any ] = {}
137152
138- # For urlencoded/multipart, use caster for oneOf/anyOf detection if validator available
153+ # For form media, select composed branches from decoded candidates.
139154 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- if one_of_schema is not None :
144- one_of_properties = self .evolve (one_of_schema ).decode (
145- location , schema_only = True
146- )
147- properties .update (one_of_properties )
155+ one_of_match = self .get_form_media_one_of_match (location )
156+ if one_of_match is not None :
157+ properties .update (one_of_match .decoded_candidate )
148158
149- any_of_schemas = self .schema_validator .iter_any_of_schemas (
150- location , caster = self .schema_caster
151- )
152- for any_of_schema in any_of_schemas :
153- any_of_properties = self .evolve (any_of_schema ).decode (
154- location , schema_only = True
155- )
156- properties .update (any_of_properties )
159+ any_of_matches = self .iter_form_media_any_of_matches (location )
160+ for any_of_match in any_of_matches :
161+ properties .update (any_of_match .decoded_candidate )
157162
158- all_of_schemas = self .schema_validator .iter_all_of_schemas (
159- location
160- )
161- for all_of_schema in all_of_schemas :
162- all_of_properties = self .evolve (all_of_schema ).decode (
163- location , schema_only = True
164- )
165- properties .update (all_of_properties )
163+ all_of_matches = self .iter_form_media_all_of_matches (location )
164+ for all_of_match in all_of_matches :
165+ properties .update (all_of_match .decoded_candidate )
166166
167167 for prop_name , prop_schema in get_properties (self .schema ).items ():
168168 try :
169169 properties [prop_name ] = self .decode_property (
170170 prop_name , prop_schema , location
171171 )
172172 except KeyError :
173- if "default" not in prop_schema :
173+ if not use_defaults or "default" not in prop_schema :
174174 continue
175175 properties [prop_name ] = (prop_schema / "default" ).read_value ()
176176
@@ -179,6 +179,80 @@ def decode(
179179
180180 return properties
181181
182+ def get_form_media_one_of_match (
183+ self ,
184+ location : Mapping [str , Any ],
185+ ) -> Optional [FormMediaSchemaMatch ]:
186+ if self .schema is None or "oneOf" not in self .schema :
187+ return None
188+
189+ for subschema in self .schema / "oneOf" :
190+ match = self .get_form_media_schema_match (subschema , location )
191+ if match is not None :
192+ return match
193+
194+ return None
195+
196+ def iter_form_media_any_of_matches (
197+ self ,
198+ location : Mapping [str , Any ],
199+ ) -> list [FormMediaSchemaMatch ]:
200+ if self .schema is None or "anyOf" not in self .schema :
201+ return []
202+
203+ return list (self .iter_form_media_schema_matches ("anyOf" , location ))
204+
205+ def iter_form_media_all_of_matches (
206+ self ,
207+ location : Mapping [str , Any ],
208+ ) -> list [FormMediaSchemaMatch ]:
209+ if self .schema is None or "allOf" not in self .schema :
210+ return []
211+
212+ return list (self .iter_form_media_schema_matches ("allOf" , location ))
213+
214+ def iter_form_media_schema_matches (
215+ self ,
216+ keyword : str ,
217+ location : Mapping [str , Any ],
218+ ) -> Iterator [FormMediaSchemaMatch ]:
219+ assert self .schema is not None
220+
221+ for subschema in self .schema / keyword :
222+ match = self .get_form_media_schema_match (subschema , location )
223+ if match is not None :
224+ yield match
225+
226+ def get_form_media_schema_match (
227+ self ,
228+ subschema : SchemaPath ,
229+ location : Mapping [str , Any ],
230+ ) -> Optional [FormMediaSchemaMatch ]:
231+ assert self .schema_validator is not None
232+
233+ deserializer = self .evolve (subschema )
234+ try :
235+ validation_decoded_candidate = deserializer .decode (
236+ location ,
237+ schema_only = True ,
238+ use_defaults = False ,
239+ )
240+ except DeserializeError :
241+ return None
242+
243+ validator = self .schema_validator .evolve (subschema )
244+ validation_candidate = dict (location )
245+ validation_candidate .update (validation_decoded_candidate )
246+
247+ try :
248+ validator .validate (validation_candidate )
249+ except ValidateError :
250+ return None
251+
252+ decoded_candidate = deserializer .decode (location , schema_only = True )
253+
254+ return FormMediaSchemaMatch (subschema , decoded_candidate )
255+
182256 def decode_property (
183257 self ,
184258 prop_name : str ,
0 commit comments