Skip to content

InvalidCastException in OpenBankingMapping.MapTransactionSummary when created_on is returned as a JSON array #97

Description

@RadoslavSheytanovGP

Summary

Retrieving the status of an Open Banking (GP-ECOM) transaction via ReportBuilder.Execute() throws an unhandled System.InvalidCastException from inside GlobalPayments.Api.dll.

The crash happens while mapping the report response when the created_on field arrives as a JSON array (parsed to List<string>) rather than a single scalar value. This exception is not caught and propagates out of the SDK, taking down the caller's execution thread.

Affected Versions

Confirmed present and unchanged in versions: 8.0.19, 9.0.0, 10.0.0, and 11.0.4 (current latest on NuGet). Upgrading does not resolve the issue as the underlying parsing logic remains identical across these releases.

Execution Flow

ReportBuilder.Execute()
└── OpenBankingProvider.ProcessReport
    └── OpenBankingMapping.MapReportResponse
        └── MapTransactionSummary

Stack Trace

System.InvalidCastException: Unable to cast object of type 'System.Collections.Generic.List`1[System.String]' to type 'System.DateTime'.
   at GlobalPayments.Api.Utils.JsonDoc.GetValue[T](String name, Func`2 converter)
   at GlobalPayments.Api.Mapping.OpenBankingMapping.MapTransactionSummary(JsonDoc response)
   at GlobalPayments.Api.Mapping.OpenBankingMapping.MapReportResponse`1(String rawResponse, ReportType reportType)
   at GlobalPayments.Api.Gateways.OpenBankingProvider.ProcessReport[T](ReportBuilder`1 builder)
   at GlobalPayments.Api.Builders.ReportBuilder`1.Execute(String configName)


Root Cause

The crash results from two interacting issues within src/GlobalPayments.Api/Utils/JsonUtils.cs:

1. Array Coercion to List<string>

In ParseObject, any field whose value is a JSON array of primitives/non-objects is eagerly stored via ParseTypeArray<string>:

else {
    var objs = ParseTypeArray<string>(child.Value as JArray, encoder);
    values.Add(child.Name, objs);   // Stored in the internal dictionary as List<string>
}

When a payload contains "created_on": ["2026-06-11T11:29:35Z"], a List<string> is stored under that dictionary key.

2. Double-Faulting in GetValue<T> Fallback

Convert.ChangeType throws an InvalidCastException because List<string> does not implement IConvertible. The catch block attempts a direct cast to T (DateTime), which immediately throws a second InvalidCastException. Because this second exception occurs inside the catch block, it escapes the method completely.

public T GetValue<T>(string name, Func<object, T> converter = null) {
    if (_dict.ContainsKey(name) && _dict[name] != null) {
        try {
            var value = _dict[name];
            // ...
            else return (T)Convert.ChangeType(value, typeof(T)); // Throws: List<string> is not IConvertible
        }                                                        
        catch (InvalidCastException) {
            return (T)_dict[name];                               // Throws AGAIN: Direct cast from List<string> to DateTime fails
        }                                                        
    }
    return default(T);
}

This path is triggered directly during transaction mapping in src/GlobalPayments.Api/Mapping/OpenBankingMapping.cs:

summary.TransactionDate = response.GetValue<DateTime>("created_on");

Steps to Reproduce

  1. Execute a ReportBuilder query to fetch an Open Banking / bank-payment transaction status.
  2. Ensure the upstream gateway maps or returns the created_on property wrapped inside a JSON array:
{
  "payments": [
    {
      "ob_trans_id": "example-id",
      "status": "CAPTURED",
      "amount": "10.00",
      "currency": "GBP",
      "created_on": ["2026-06-11T11:29:35Z"]
    }
  ]
}
  1. The SDK will fail immediately with the InvalidCastException stack trace instead of returning a populated TransactionSummary object.

Expected Behavior

The SDK must handle type anomalies gracefully without throwing unhandled infrastructure exceptions. It should either:

  • Extract and parse the first element when an array is unexpectedly provided for a scalar property.
  • Degrade gracefully by returning default(T) if the internal data structure cannot be cast to the target type.

Suggested Fix

Tip

Resolving Fix 1 offers the highest value across the SDK, preventing unhandled dictionary casting failures for all types. Implementing Fix 2 guarantees that unexpected Open Banking array variants map to valid dates instead of zeroing out.

Fix 1: Harden GetValue<T> and GetNullableValue<T>

Safeguard the fallback path using a pattern matching or safe type check compatible with .NET Standard 1.3:

catch (InvalidCastException) {
    if (_dict[name] is T variable) return variable;
    return default(T);
}

Fix 2: Implement Array Tolerance in Open Banking Mappings

Update MapTransactionSummary to safely intercept and unpack string collections:

summary.TransactionDate = response.GetValue<DateTime>("created_on", o =>
    Convert.ToDateTime(o is System.Collections.IList list ? list[0] : o));

Note

This issue addresses SDK resilience. A separate engineering review should evaluate why the Open Banking gateway serialization pipeline wrapped the created_on timestamp in an array context in the first place.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions