Skip to content

Failure to parse MonetarySavingsAccount that does not have a savings goal set #179

@zlobober

Description

@zlobober

Hi!

I noticed that SDK of version 1.28 fails to parse monetary savings accounts that do not have a savings goal.

How to reproduce:

  1. Create a sandbox test user
  2. Create a savings account for it (I did it from the bunq mobile app)
  3. The output of curl -s -X GET "https://public-api.sandbox.bunq.com/v1/user/<user_id>/monetary-account-savings" looks like this:
  {
    "Response": [
      {
        "MonetaryAccountSavings": {
          "id": <user_id>,
          "savings_goal": null,
          "savings_goal_progress": null,
          // ... other fields
        }
      }
    ]
  }
  1. The following reproduce script fails:
#!/usr/bin/env python3
from bunq.sdk.context.api_context import ApiContext
from bunq.sdk.context.bunq_context import BunqContext
from bunq.sdk.model.generated.endpoint import MonetaryAccountSavingsApiObject
from bunq import ApiEnvironmentType

# Setup with sandbox API key
API_KEY = "<api_key>"
api_context = ApiContext.create(ApiEnvironmentType.SANDBOX, API_KEY, "test")
BunqContext.load_api_context(api_context)

# This call fails with: TypeError: float() argument must be a string or a real number, not 'NoneType'
# Error occurs in bunq/sdk/json/float_adapter.py line 16: return float(string)
# when trying to parse null savings_goal_progress as float
savings_accounts = MonetaryAccountSavingsApiObject.list()
print(f"Success: {len(savings_accounts.value)} savings accounts found")

with a stacktrace like this:

Traceback (most recent call last):
  File "/home/max/accounting/bunq_crawler/minimal_repro.py", line 14, in <module>
    savings_accounts = MonetaryAccountSavingsApiObject.list()
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/max/.local/lib/python3.11/site-packages/bunq/sdk/model/generated/endpoint.py", line 26454, in list
    cls._from_json_list(response_raw, cls._OBJECT_TYPE_GET)
  File "/home/max/.local/lib/python3.11/site-packages/bunq/sdk/model/core/bunq_model.py", line 111, in _from_json_list
    item_deserialized = converter.deserialize(cls, item_unwrapped)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/max/.local/lib/python3.11/site-packages/bunq/sdk/json/converter.py", line 464, in deserialize
    return JsonAdapter.deserialize(cls, obj_raw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/max/.local/lib/python3.11/site-packages/bunq/sdk/json/converter.py", line 102, in deserialize
    return cls._deserialize_default(cls_target, obj_raw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/max/.local/lib/python3.11/site-packages/bunq/sdk/json/converter.py", line 117, in _deserialize_default
    return cls._deserialize_dict(cls_target, obj_raw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/max/.local/lib/python3.11/site-packages/bunq/sdk/json/converter.py", line 147, in _deserialize_dict
    dict_deserialized = cls._deserialize_dict_attributes(cls_target, dict_)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/max/.local/lib/python3.11/site-packages/bunq/sdk/json/converter.py", line 163, in _deserialize_dict_attributes
    dict_deserialized[value_specs.name] = cls._deserialize_value(value_specs.types, dict_[key])
                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/max/.local/lib/python3.11/site-packages/bunq/sdk/json/converter.py", line 307, in _deserialize_value
    return cls.deserialize(types.main, value)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/max/.local/lib/python3.11/site-packages/bunq/sdk/json/converter.py", line 104, in deserialize
    return deserializer.deserialize(cls_target, obj_raw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/max/.local/lib/python3.11/site-packages/bunq/sdk/json/float_adapter.py", line 16, in deserialize
    return float(string)
           ^^^^^^^^^^^^^
TypeError: float() argument must be a string or a real number, not 'NoneType'

I verified using a debugger that the issue is indeed in the savings_goal_progress key. I thought about bringing a PR with a fix, but I am not entirely sure, whether it is an SDK bug (it should handle nulls gracefully), or an API issue (it should not emit keys with null values). According to the documentation, the field "savings_goal_progress" is optional, but it confuses me that I don't see any examples of handling optional primitive values around the sdk/json.

Workaround: setting a saving goal to any amount.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions