From 8cbecdec0ed516e8dac8f2800fd8f5d25a20f319 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 17 Jun 2026 07:34:39 -0400 Subject: [PATCH] fix(inbound): materialize array params as typed lists, not JsonElement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An inbound /api array parameter was materialized as List whose elements were raw System.Text.Json.JsonElement. When such a value is routed Central->Site and a template script assigns it to a List-typed Galaxy attribute (recv.Attributes[name] = Parameters[name]), the script-side encode stalls (the attribute codec JSON-serializing JsonElement items) and the array write never reaches the DCL — the Ipsen MoveIn array writes hung 30s while scalars succeeded. ParameterValidator.MaterializeArray now builds a strongly-typed list per the declared element schema (List/long/double/bool); arrays with no declared scalar element type materialize each element to its CLR value (MaterializeJsonValue) so no raw JsonElement survives. Typed lists serialize cleanly across nodes and encode to a canonical JSON array, which the InstanceActor decodes back to the typed list for the device write. --- .../ParameterValidator.cs | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ParameterValidator.cs b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ParameterValidator.cs index 75790067..f36ff743 100644 --- a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ParameterValidator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ParameterValidator.cs @@ -114,10 +114,78 @@ public static class ParameterValidator "number" => element.GetDouble(), "string" => element.GetString(), "object" => JsonSerializer.Deserialize>(element.GetRawText()), - "array" => JsonSerializer.Deserialize>(element.GetRawText()), + "array" => MaterializeArray(element, schema.Items), _ => JsonSerializer.Deserialize(element.GetRawText()), }; } + + /// + /// Materializes a JSON array to a STRONGLY-TYPED list (List<string>, + /// List<long>, List<double>, List<bool>) per the element schema, + /// rather than a List<object?> of raw . The + /// raw-JsonElement form is not cleanly Akka-serializable when the parameter is + /// routed Central→Site, and a script writing it to a List attribute stalls when + /// the attribute codec tries to JSON-serialize JsonElement items. A typed list + /// serializes cleanly across nodes and encodes to a canonical JSON array, which + /// the InstanceActor decodes back to the typed list for the device write. + /// + private static object? MaterializeArray(JsonElement element, InboundApiSchema? items) + { + // When the element type is a declared scalar, build a STRONGLY-TYPED list + // (List/long/double/bool) — the cleanest form to route Central→Site + // and to encode to a canonical JSON array for a List attribute write. + switch (items?.Type) + { + case "integer": + { + var list = new List(element.GetArrayLength()); + foreach (var e in element.EnumerateArray()) list.Add(e.GetInt64()); + return list; + } + case "number": + { + var list = new List(element.GetArrayLength()); + foreach (var e in element.EnumerateArray()) list.Add(e.GetDouble()); + return list; + } + case "boolean": + { + var list = new List(element.GetArrayLength()); + foreach (var e in element.EnumerateArray()) list.Add(e.GetBoolean()); + return list; + } + case "string": + { + var list = new List(element.GetArrayLength()); + foreach (var e in element.EnumerateArray()) + list.Add(e.ValueKind == JsonValueKind.Null ? string.Empty : e.GetString() ?? string.Empty); + return list; + } + default: + { + // No declared (or non-scalar) element type: materialize each element + // to its CLR value so no raw JsonElement survives (raw JsonElement is + // not cleanly Akka-serializable and stalls the List-attribute encode). + var list = new List(element.GetArrayLength()); + foreach (var e in element.EnumerateArray()) list.Add(MaterializeJsonValue(e)); + return list; + } + } + } + + /// Recursively converts a to a plain CLR value + /// (string/long/double/bool/null, or nested List/Dictionary) — never a JsonElement. + private static object? MaterializeJsonValue(JsonElement e) => e.ValueKind switch + { + JsonValueKind.String => e.GetString(), + JsonValueKind.Number => e.TryGetInt64(out var l) ? l : e.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + JsonValueKind.Array => e.EnumerateArray().Select(MaterializeJsonValue).ToList(), + JsonValueKind.Object => e.EnumerateObject().ToDictionary(p => p.Name, p => MaterializeJsonValue(p.Value)), + _ => e.GetRawText(), + }; } ///