jsons icon indicating copy to clipboard operation
jsons copied to clipboard

DeserializationError when loading into a class with an Optional bson.ObjectID

Open yurirocha15 opened this issue 2 years ago • 2 comments

Hello, I encountered an issue when loading data into a class with an Optional ObjectID field.

Environment

Python 3.8.5 jsons 1.6.3

Issue

  • If a class has a non-optional ObjectID field, jsons works fine.
  • If a class has an optional ObjectID field, but the field is "None" in the dictionary, jsons also works fine.
  • However, when loading an ObjectID into an Optional[ObjectID] field, the following error happens:
Traceback (most recent call last):
  File "/home/yuri/ex.py", line 25, in <module>
    print(jsons.load(data2, TestData))
  File "/home/yuri/.pyenv/versions/3.8.5/lib/python3.8/site-packages/jsons/_load_impl.py", line 99, in load
    return _do_load(json_obj, deserializer, cls, initial, **kwargs_)
  File "/home/yuri/.pyenv/versions/3.8.5/lib/python3.8/site-packages/jsons/_load_impl.py", line 111, in _do_load
    result = deserializer(json_obj, cls, **kwargs)
  File "/home/yuri/.pyenv/versions/3.8.5/lib/python3.8/site-packages/jsons/deserializers/default_object.py", line 40, in default_object_deserializer
    constructor_args = _get_constructor_args(obj, cls, **kwargs)
  File "/home/yuri/.pyenv/versions/3.8.5/lib/python3.8/site-packages/jsons/deserializers/default_object.py", line 64, in _get_constructor_args
    key, value = _get_value_for_attr(obj=obj,
  File "/home/yuri/.pyenv/versions/3.8.5/lib/python3.8/site-packages/jsons/deserializers/default_object.py", line 94, in _get_value_for_attr
    result = sig_key, _get_value_from_obj(obj, cls, sig, sig_key,
  File "/home/yuri/.pyenv/versions/3.8.5/lib/python3.8/site-packages/jsons/deserializers/default_object.py", line 140, in _get_value_from_obj
    value = load(obj[sig_key], cls_, meta_hints=new_hints, **kwargs)
  File "/home/yuri/.pyenv/versions/3.8.5/lib/python3.8/site-packages/jsons/_load_impl.py", line 82, in load
    cls, meta_hints = _check_and_get_cls_and_meta_hints(
  File "/home/yuri/.pyenv/versions/3.8.5/lib/python3.8/site-packages/jsons/_load_impl.py", line 198, in _check_and_get_cls_and_meta_hints
    raise DeserializationError(msg, json_obj, cls)
jsons.exceptions.DeserializationError: Invalid type: "bson.objectid.ObjectId", only arguments of the following types are allowed: str, int, float, bool, list, tuple, set, dict, NoneType

Reproducing the Error

Here is an example script where this error is happening:

from dataclasses import dataclass
from typing import Optional

import jsons
from bson.objectid import ObjectId

@dataclass
class TestData:
    """Test Data"""

    non_optional_id: ObjectId
    optional_id: Optional[ObjectId] = None

data1 = {
    "non_optional_id": ObjectId(),
}

data2 = {
    "non_optional_id": ObjectId(),
    "optional_id": ObjectId(),
}

# prints: TestData(non_optional_id=ObjectId('62b1411ddde5c1f9fc9842a1'), optional_id=None)
print(jsons.load(data1, TestData)) 
# jsons.exceptions.DeserializationError: Invalid type: "bson.objectid.ObjectId", only arguments of the following types are allowed: str, int, float, bool, list, tuple, set, dict, NoneType
print(jsons.load(data2, TestData)) 

yurirocha15 avatar Jun 20 '22 10:06 yurirocha15

Hi @yurirocha15 ,

You are trying to load a "half json-serialized" object; a dict that contains objects that are not json-compatible (bson.objectid.ObjectId instances).

I traced down what happened. In non-strict mode, when an object is loaded into a class of which it already is an instance (e.g. load("this is a string", str)), the loading is skipped and that instance is returned instantly. This is what happens with your data1. With data2, it compares the type ObjectId with Optional which are inequal, therefore it is not skipped. Then, before actually trying to deserialize an ObjectId, it stops as ObjectId is not a json value (one of str, int, float, bool, list, tuple, set, dict, NoneType).

Do you really need to load a "half json-serialized" object, or would it be possible in your case to just do the following?

TestData(**data2)

ramonhagenaars avatar Jul 27 '22 19:07 ramonhagenaars

This would be possible in the example I posted but not in my real use case. I am parsing a nested dictionary into nested dataclasses; this optional parameter is in the deeper layers of the dictionary. Therefore, using the root dataclass constructor directly is not an option for me.

Is there any way to solve this using custom serializers/deserializers?

yurirocha15 avatar Jul 29 '22 04:07 yurirocha15