fix(inbound): materialize array params as typed lists, not JsonElement

An inbound /api array parameter was materialized as List<object?> 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<string>/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.
This commit is contained in:
Joseph Doherty
2026-06-17 07:34:39 -04:00
parent 45b23476fc
commit 8cbecdec0e
@@ -114,10 +114,78 @@ public static class ParameterValidator
"number" => element.GetDouble(),
"string" => element.GetString(),
"object" => JsonSerializer.Deserialize<Dictionary<string, object?>>(element.GetRawText()),
"array" => JsonSerializer.Deserialize<List<object?>>(element.GetRawText()),
"array" => MaterializeArray(element, schema.Items),
_ => JsonSerializer.Deserialize<object?>(element.GetRawText()),
};
}
/// <summary>
/// Materializes a JSON array to a STRONGLY-TYPED list (List&lt;string&gt;,
/// List&lt;long&gt;, List&lt;double&gt;, List&lt;bool&gt;) per the element schema,
/// rather than a <c>List&lt;object?&gt;</c> of raw <see cref="JsonElement"/>. 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.
/// </summary>
private static object? MaterializeArray(JsonElement element, InboundApiSchema? items)
{
// When the element type is a declared scalar, build a STRONGLY-TYPED list
// (List<string>/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<long>(element.GetArrayLength());
foreach (var e in element.EnumerateArray()) list.Add(e.GetInt64());
return list;
}
case "number":
{
var list = new List<double>(element.GetArrayLength());
foreach (var e in element.EnumerateArray()) list.Add(e.GetDouble());
return list;
}
case "boolean":
{
var list = new List<bool>(element.GetArrayLength());
foreach (var e in element.EnumerateArray()) list.Add(e.GetBoolean());
return list;
}
case "string":
{
var list = new List<string>(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<object?>(element.GetArrayLength());
foreach (var e in element.EnumerateArray()) list.Add(MaterializeJsonValue(e));
return list;
}
}
}
/// <summary>Recursively converts a <see cref="JsonElement"/> to a plain CLR value
/// (string/long/double/bool/null, or nested List/Dictionary) — never a JsonElement.</summary>
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(),
};
}
/// <summary>