-
-
Notifications
You must be signed in to change notification settings - Fork 17
Description
Fantastic library! Also, I kinda love your conversion system. It's fallback mechanism when the input data is not the same type as the source of the converter function is really beautiful.
I have just started using your library in a larger project and ran into a problem with recursion and unintuitive (for me) behavior. This library has extensive documentation, so it is possible I missed something though.
I need to support deserializing a bunch of scalar JSON types to other scalar JSON types which do not seem supported by apischema out of the box, so I registered some conversions and tested it out.
import apischema
from dataclasses import dataclass
apischema.conversions.reset_deserializers(float)
apischema.deserializer(
apischema.conversions.Conversion(apischema.identity, float, float)
)
apischema.deserializer(apischema.conversions.Conversion(float, str, float))
apischema.deserializer(apischema.conversions.Conversion(float, bool, float))
apischema.deserializer(apischema.conversions.Conversion(float, int, float))
apischema.conversions.reset_deserializers(int)
apischema.deserializer(apischema.conversions.Conversion(apischema.identity, int, int))
apischema.deserializer(apischema.conversions.Conversion(int, str, int))
apischema.deserializer(apischema.conversions.Conversion(int, bool, int))
apischema.conversions.reset_deserializers(str)
apischema.deserializer(apischema.conversions.Conversion(apischema.identity, str, str))
apischema.deserializer(apischema.conversions.Conversion(str, int, str))
apischema.deserializer(apischema.conversions.Conversion(str, float, str))
@dataclass
class Test:
my_field: str
print(apischema.deserialize(Test, {"my_field": 1.2}))Output: RecursionError: maximum recursion depth exceeded
So what seems to happen currently is that this will go through the conversions with str as target and hit (int -> str) and then jump to the conversions for target type of int and hit (str -> int) and then we keep looping.
The solution I am currently using is to monkeypatch the ConversionUnionMethod.deserialize method so it tries to find Conversion alternatives which are exact matches for the type of the given data before falling back to your current algorithm. So something like the following...
_old_conversion_union_method_deserialize = ConversionUnionMethod.deserialize
_method_types = {
StrMethod: str,
ConstrainedStrMethod: str,
BoolMethod: bool,
IntMethod: int,
ConstrainedIntMethod: int,
FloatMethod: float,
ConstrainedFloatMethod: float,
NoneMethod: None,
}
def _conversion_union_method_deserialize(self, data: Any) -> Any:
# try to find exact match for a conversion source type matching the given data type
# if found go with that instead of trying one at a time and trying to convert
# the data to whatever source type there is for each conversion
# we fallback to the builtin method which just tries one at a time if no exact match
error = None
for alternative in self.alternatives:
method_class = alternative.method.__class__
if alternative.method.__class__ is ConversionUnionMethod:
method_class = alternative.method.alternatives[0].method.__class__
try:
type_ = _method_types[method_class]
except KeyError:
continue
if not isinstance(data, type_):
continue
try:
value = alternative.method.deserialize(data)
except ValidationError as err:
error = merge_errors(error, err)
continue
try:
return alternative.converter(value)
except ValidationError as err:
error = merge_errors(error, err)
except ValueError as err:
if not alternative.value_error:
raise
error = merge_errors(error, ValidationError(str(err)))
return _old_conversion_union_method_deserialize(self, data)
ConversionUnionMethod.deserialize = _conversion_union_method_deserializeThis is not exactly elegant, but it seems to work. The source types are not available in the ConversionAlternative objects here, so a hack is to use the method and map that to a type.
I don't have the time to create a pull request but i thought this might help someone and might be something you would be interested in adding. Alternatively, if you have a better approach to solve the same problem, I would be interested.