Skip to content

SDK crashes on get_status_by_barcode(detail=True) due to inconsistent JSON types for OldStatus and Shipment fields #5

@Mortezamahdavis

Description

@Mortezamahdavis

SDK version

postnl-ecommerce-sdk 1.0.5


Summary

Calling get_status_by_barcode(..., detail=True) raises an AttributeError for the vast majority of barcodes. Two distinct bugs were found, both caused by the same root cause: the PostNL API serialises XML-to-JSON in a way that returns a single object {} when there is one entry, and an array [{}, {}] when there are multiple. The SDK does not handle the single-object case.


Bug #1OldStatus returned as dict instead of list

Root cause

In complete_status_shipment.py the SDK always iterates OldStatus as a list:

# postnlecommerce/models/complete_status_shipment.py – lines 203-207
old_status = None
if dictionary.get('OldStatus') is not None:
    old_status = [OldStatus.from_dictionary(x) for x in dictionary.get('OldStatus')]
else:
    old_status = APIHelper.SKIP

When the shipment has exactly one previous status, the API returns OldStatus as a plain JSON object:

"OldStatus": {
  "TimeStamp": "17-01-2026 19:07:32.778",
  "StatusCode": "1",
  "StatusDescription": "Zending voorgemeld",
  "PhaseCode": "1",
  "PhaseDescription": "Collectie"
}

When iterated as for x in dict, Python yields the dict's keys (strings like "TimeStamp", "StatusCode", …). OldStatus.from_dictionary("TimeStamp") is then called with a str, crashing immediately in old_status.py:

# postnlecommerce/models/old_status.py – line 83
time_stamp = dictionary.get("TimeStamp") if dictionary.get("TimeStamp") else APIHelper.SKIP
#                       ^^^ AttributeError: 'str' object has no attribute 'get'

Full traceback

File ".../postnlecommerce/controllers/shipping_status_controller.py", line 103, in get_status_by_barcode
    ).execute()
File ".../apimatic_core/response_handler.py", line 100, in apply_deserializer
    return self._deserializer(response.text, self._deserialize_into)
File ".../postnlecommerce/models/shippingstatus_response.py", line 78, in from_dictionary
    complete_status = CompleteStatus.from_dictionary(...)
File ".../postnlecommerce/models/complete_status.py", line 60, in from_dictionary
    shipment = CompleteStatusShipment.from_dictionary(...)
File ".../postnlecommerce/models/complete_status_shipment.py", line 205, in from_dictionary
    old_status = [OldStatus.from_dictionary(x) for x in dictionary.get('OldStatus')]
File ".../postnlecommerce/models/old_status.py", line 83, in from_dictionary
    time_stamp = dictionary.get("TimeStamp") if dictionary.get("TimeStamp") else APIHelper.SKIP
AttributeError: 'str' object has no attribute 'get'

Suggested fix — complete_status_shipment.py

old_status_raw = dictionary.get('OldStatus')
if old_status_raw is not None:
    if isinstance(old_status_raw, dict):   # single entry → wrap in list
        old_status_raw = [old_status_raw]
    old_status = [OldStatus.from_dictionary(x) for x in old_status_raw]
else:
    old_status = APIHelper.SKIP

Bug #2Shipment returned as list instead of dict

Root cause

In complete_status.py the SDK always treats Shipment as a single object:

# postnlecommerce/models/complete_status.py – line 60
shipment = CompleteStatusShipment.from_dictionary(dictionary.get('Shipment')) if 'Shipment' in dictionary.keys() else APIHelper.SKIP

When a barcode covers multiple shipments (e.g. a multi-collo shipment), the API returns Shipment as a JSON array:

"Shipment": [ { ... }, { ... }, { ... } ]

CompleteStatusShipment.from_dictionary([...]) receives a list, and the first .get() call on it crashes:

AttributeError: 'list' object has no attribute 'get'

Suggested fix — complete_status.py

shipment_raw = dictionary.get('Shipment')
if shipment_raw is not None:
    if isinstance(shipment_raw, list):
        # multi-collo: take first shipment or handle all as needed
        shipment = CompleteStatusShipment.from_dictionary(shipment_raw[0])
    else:
        shipment = CompleteStatusShipment.from_dictionary(shipment_raw)
else:
    shipment = APIHelper.SKIP

Alternatively, the model could expose a shipments list field to preserve all entries.


Minimal reproducible example

from postnlecommerce.configuration import Environment
from postnlecommerce.http.auth.custom_header_authentication import CustomHeaderAuthenticationCredentials
from postnlecommerce.postnlecommerce_client import PostnlecommerceClient
from postnlecommerce.models.language_enum import LanguageEnum

client = PostnlecommerceClient(
    custom_header_authentication_credentials=CustomHeaderAuthenticationCredentials(
        apikey='<YOUR_API_KEY>'
    ),
    environment=Environment.PRODUCTION_SERVER
)

# Bug  — barcode with exactly one previous status
client.shipping_status.get_status_by_barcode(
    'BARCODE_FAIL_A/B', detail=True, language=LanguageEnum.NL, max_days=None
)

Working control example (SDK succeeds)

Barcode:
BARCODE_OK_C

Observed response classification:

HTTP status: 200
Shipment type: dict
OldStatus type: list
OldStatus length: 11
Sample output (parsed from API response):

First OldStatus item:
{"TimeStamp":"09-01-2026 00:49:12","StatusCode":"99","StatusDescription":"Niet van toepassing","PhaseCode":"4","PhaseDescription":"Afgeleverd"}
Current Status:
{"TimeStamp":"11-01-2026 03:00:00","StatusCode":"11","StatusDescription":"Zending afgeleverd","PhaseCode":"4","PhaseDescription":"Afgeleverd"}
Why this works:
This barcode returns OldStatus as an array (multiple entries), which matches what the SDK expects.
The failing barcodes mostly return OldStatus as a single object, or Shipment as an array, which the SDK currently does not handle.

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