11from typing import Any
2+ from typing import Dict
23from typing import Generic
34from typing import Iterable
45from typing import List
1112from jsonschema_path import SchemaPath
1213
1314from openapi_core .casting .schemas .exceptions import CastError
15+ from openapi_core .deserializing .media_types .enums import DataKind
1416from openapi_core .schema .schemas import get_properties
1517from openapi_core .util import forcebool
1618from openapi_core .validation .schemas .validators import SchemaValidator
@@ -27,7 +29,12 @@ def __init__(
2729 self .schema_validator = schema_validator
2830 self .schema_caster = schema_caster
2931
30- def __call__ (self , value : Any ) -> Any :
32+ def __call__ (
33+ self ,
34+ value : Any ,
35+ data_kind : DataKind = DataKind .STRINGLY ,
36+ property_data_kinds : Optional [Dict [str , DataKind ]] = None
37+ ) -> Any :
3138 return value
3239
3340
@@ -37,16 +44,22 @@ def __call__(self, value: Any) -> Any:
3744class PrimitiveTypeCaster (Generic [PrimitiveType ], PrimitiveCaster ):
3845 primitive_type : Type [PrimitiveType ] = NotImplemented
3946
40- def __call__ (self , value : Union [str , bytes ]) -> Any :
47+ def __call__ (
48+ self ,
49+ value : Union [str , bytes ],
50+ data_kind : DataKind = DataKind .STRINGLY ,
51+ property_data_kinds : Optional [Dict [str , DataKind ]] = None
52+ ) -> Any :
53+ # STRINGLY: only coerce from str/bytes, return others unchanged
54+ if data_kind == DataKind .STRINGLY :
55+ if not isinstance (value , (str , bytes )):
56+ return value
57+
4158 self .validate (value )
4259
4360 return self .primitive_type (value ) # type: ignore [call-arg]
4461
4562 def validate (self , value : Any ) -> None :
46- # FIXME: don't cast data from media type deserializer
47- # See https://github.com/python-openapi/openapi-core/issues/706
48- # if not isinstance(value, (str, bytes)):
49- # raise ValueError("should cast only from string or bytes")
5063 pass
5164
5265
@@ -61,19 +74,29 @@ class NumberCaster(PrimitiveTypeCaster[float]):
6174class BooleanCaster (PrimitiveTypeCaster [bool ]):
6275 primitive_type = bool
6376
64- def __call__ (self , value : Union [str , bytes ]) -> Any :
77+ def __call__ (
78+ self ,
79+ value : Union [str , bytes ],
80+ data_kind : DataKind = DataKind .STRINGLY ,
81+ property_data_kinds : Optional [Dict [str , DataKind ]] = None
82+ ) -> Any :
83+ # STRINGLY: only coerce from str/bytes, return others unchanged
84+ if data_kind == DataKind .STRINGLY :
85+ if not isinstance (value , (str , bytes )):
86+ return value
87+
6588 self .validate (value )
6689
6790 return self .primitive_type (forcebool (value ))
6891
6992 def validate (self , value : Any ) -> None :
70- super ().validate (value )
71-
72- # FIXME: don't cast data from media type deserializer
73- # See https://github.com/python-openapi/openapi-core/issues/706
93+ # Already a boolean, accept it
7494 if isinstance (value , bool ):
7595 return
7696
97+ if not isinstance (value , (str , bytes )):
98+ return
99+
77100 if value .lower () not in ["false" , "true" ]:
78101 raise ValueError ("not a boolean format" )
79102
@@ -85,20 +108,36 @@ def items_caster(self) -> "SchemaCaster":
85108 items_schema = self .schema .get ("items" , SchemaPath .from_dict ({}))
86109 return self .schema_caster .evolve (items_schema )
87110
88- def __call__ (self , value : Any ) -> List [Any ]:
111+ def __call__ (
112+ self ,
113+ value : Any ,
114+ data_kind : DataKind = DataKind .STRINGLY ,
115+ property_data_kinds : Optional [Dict [str , DataKind ]] = None
116+ ) -> List [Any ]:
89117 # str and bytes are not arrays according to the OpenAPI spec
90118 if isinstance (value , (str , bytes )) or not isinstance (value , Iterable ):
91119 raise CastError (value , self .schema ["type" ])
92120
121+ items_schema = self .schema .getkey ("items" )
122+ items_caster = self .schema_caster .evolve (items_schema )
123+ # Array items don't have property names, pass empty policies dict
93124 try :
94- return list (map (self .items_caster .cast , value ))
125+ return list (
126+ map (lambda item : items_caster .cast (item , data_kind = data_kind , property_data_kinds = {}), value )
127+ )
95128 except (ValueError , TypeError ):
96129 raise CastError (value , self .schema ["type" ])
97130
98131
99132class ObjectCaster (PrimitiveCaster ):
100- def __call__ (self , value : Any ) -> Any :
101- return self ._cast_proparties (value )
133+ def __call__ (
134+ self ,
135+ value : Any ,
136+ data_kind : DataKind = DataKind .STRINGLY ,
137+ property_data_kinds : Optional [Dict [str , DataKind ]] = None
138+ ) -> Any :
139+ return self ._cast_proparties (value , schema_only = False , data_kind = data_kind ,
140+ property_data_kinds = property_data_kinds or {})
102141
103142 def evolve (self , schema : SchemaPath ) -> "ObjectCaster" :
104143 cls = self .__class__
@@ -109,14 +148,24 @@ def evolve(self, schema: SchemaPath) -> "ObjectCaster":
109148 self .schema_caster .evolve (schema ),
110149 )
111150
112- def _cast_proparties (self , value : Any , schema_only : bool = False ) -> Any :
151+ def _cast_proparties (
152+ self ,
153+ value : Any ,
154+ schema_only : bool = False ,
155+ data_kind : DataKind = DataKind .STRINGLY ,
156+ property_data_kinds : Optional [Dict [str , DataKind ]] = None
157+ ) -> Any :
113158 if not isinstance (value , dict ):
114159 raise CastError (value , self .schema ["type" ])
115160
161+ if property_data_kinds is None :
162+ property_data_kinds = {}
163+
116164 all_of_schemas = self .schema_validator .iter_all_of_schemas (value )
117165 for all_of_schema in all_of_schemas :
118166 all_of_properties = self .evolve (all_of_schema )._cast_proparties (
119- value , schema_only = True
167+ value , schema_only = True , data_kind = data_kind ,
168+ property_data_kinds = property_data_kinds
120169 )
121170 value .update (all_of_properties )
122171
@@ -125,8 +174,14 @@ def _cast_proparties(self, value: Any, schema_only: bool = False) -> Any:
125174 prop_value = value [prop_name ]
126175 except KeyError :
127176 continue
177+
178+ # Use property-specific policy if available, otherwise inherit parent policy
179+ prop_data_kind = property_data_kinds .get (prop_name , data_kind )
180+
128181 value [prop_name ] = self .schema_caster .evolve (prop_schema ).cast (
129- prop_value
182+ prop_value ,
183+ data_kind = prop_data_kind ,
184+ property_data_kinds = {} # Policies don't cascade to nested objects
130185 )
131186
132187 if schema_only :
@@ -150,7 +205,7 @@ def _cast_proparties(self, value: Any, schema_only: bool = False) -> Any:
150205 for prop_name , prop_value in value .items ():
151206 if prop_name in value :
152207 continue
153- value [prop_name ] = additional_prop_caster .cast (prop_value )
208+ value [prop_name ] = additional_prop_caster .cast (prop_value , data_kind = data_kind )
154209
155210 return value
156211
@@ -197,7 +252,16 @@ def __init__(
197252
198253 self .types_caster = types_caster
199254
200- def cast (self , value : Any ) -> Any :
255+ def cast (
256+ self ,
257+ value : Any ,
258+ data_kind : DataKind = DataKind .STRINGLY ,
259+ property_data_kinds : Optional [Dict [str , DataKind ]] = None ,
260+ ) -> Any :
261+ # TYPED data: return value unchanged, no casting applied
262+ if data_kind == DataKind .TYPED :
263+ return value
264+
201265 # skip casting for nullable in OpenAPI 3.0
202266 if value is None and self .schema .getkey ("nullable" , False ):
203267 return value
@@ -210,7 +274,7 @@ def cast(self, value: Any) -> Any:
210274 return value
211275
212276 try :
213- return type_caster (value )
277+ return type_caster (value , data_kind = data_kind , property_data_kinds = property_data_kinds )
214278 except (ValueError , TypeError ):
215279 raise CastError (value , schema_type )
216280
0 commit comments