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(), + }; } ///