ramonhagenaars / jsons

🐍 A Python lib for (de)serializing Python objects to/from JSON
https://jsons.readthedocs.io
MIT License
289 stars 41 forks source link

Deserializing Union[List[str], str] creates list of each char in str #104

Closed scottee closed 4 years ago

scottee commented 4 years ago

If I have a data class with a field where the type is a List[str] or Union[List[str],..], when the list deserializer runs on a json value that is a string, it turns the string into a list of chars of the string.

The problem might well be in default_list.py:35 and 38. If the input object is iterable, like a string, it will succeed in deserializing as a list, when it shouldn't. Some type checking is needed there to make sure the input truly is a list.

The opposite seems to also be a bug for type Union[str, List[str]] or str, when the json obj is a list. At default_primitive.py:18 just tries to do "str(obj)" When obj is a list, this will not fail as it should. Some type checking is needed to make sure the obj truy is a str.

If there is a workaround, I'd really like to know it, because right now I'm blocked.

ramonhagenaars commented 4 years ago

Hi @scottee .

What you are describing is not a bug but expected behavior on jsons' part.

You can load an object with any class and jsons will try its best to load that object into an instance of that class. The union deserializer tries to deserialize an object to its types in the order from left to right. Thus, loading a list of strings into a Union[str, List[str]] will behave as you experienced.

You can of course redefine this behavior yourself, by overriding the union deserializer. Here is a code snippet that does just that:

    import jsons
    import typish

    def func(obj, cls, **kwargs):
        for cls_ in cls.__args__:
            if typish.instance_of(obj, cls_):
                return jsons.load(obj, cls_)
        raise jsons.DeserializationError(f'Cannot deserialize {obj} as {cls}')

    # Set the custom deserializer and override the default.
    jsons.set_deserializer(func=func, cls=Union)

    # Some example deserializations.
    should_be_list = jsons.load(['testing'], Union[List[str], str])
    should_be_list_as_well = jsons.load(['testing'], Union[str, List[str]])
    should_be_str = jsons.load('testing', Union[List[str], str])
    should_be_str_as_well = jsons.load('testing', Union[str, List[str]])

Another option to consider, if you have the luxury, is to abondon the interface in which an attribute can be either a string or a list of strings altogether. One might argue in favor of an interface that merely accepts a list of strings.

Hope this helps.