diff --git a/src/ScadaLink.Commons/Serialization/OpcUaEndpointConfigSerializer.cs b/src/ScadaLink.Commons/Serialization/OpcUaEndpointConfigSerializer.cs index 79f6ff3..60b305d 100644 --- a/src/ScadaLink.Commons/Serialization/OpcUaEndpointConfigSerializer.cs +++ b/src/ScadaLink.Commons/Serialization/OpcUaEndpointConfigSerializer.cs @@ -118,8 +118,27 @@ public static class OpcUaEndpointConfigSerializer private static OpcUaEndpointConfig LoadLegacy(string json) { - var dict = JsonSerializer.Deserialize>(json) - ?? new Dictionary(); + using var doc = JsonDocument.Parse(json); + if (doc.RootElement.ValueKind != JsonValueKind.Object) + throw new JsonException("Legacy JSON must be a flat object."); + + var dict = new Dictionary(); + foreach (var prop in doc.RootElement.EnumerateObject()) + { + // JsonElement.ToString() returns the raw value for primitives: + // "1000" for numbers, "true"/"false" for booleans, the string content + // for strings. Nested objects/arrays serialize to their JSON; we don't + // expect those in the legacy flat-dict shape, but FromFlatDict will + // simply ignore unknown keys. + dict[prop.Name] = prop.Value.ValueKind switch + { + JsonValueKind.String => prop.Value.GetString() ?? "", + JsonValueKind.Number => prop.Value.GetRawText(), + JsonValueKind.True => "True", + JsonValueKind.False => "False", + _ => prop.Value.ToString() + }; + } return FromFlatDict(dict); } diff --git a/tests/ScadaLink.Commons.Tests/Types/DataConnections/OpcUaEndpointConfigSerializerTests.cs b/tests/ScadaLink.Commons.Tests/Types/DataConnections/OpcUaEndpointConfigSerializerTests.cs index ea3b660..538cbfa 100644 --- a/tests/ScadaLink.Commons.Tests/Types/DataConnections/OpcUaEndpointConfigSerializerTests.cs +++ b/tests/ScadaLink.Commons.Tests/Types/DataConnections/OpcUaEndpointConfigSerializerTests.cs @@ -114,6 +114,27 @@ public class OpcUaEndpointConfigSerializerTests Assert.Equal("opc.tcp://x:4840", config.EndpointUrl); } + [Fact] + public void Deserialize_LegacyWithMixedTypeValues_PreservesAllFields() + { + // Real-world legacy producer emitted mixed JSON value types: + // string for endpoint, number for publishInterval, etc. + var legacyJson = """ + {"endpoint":"opc.tcp://scadalink-opcua:50000","securityMode":"None","publishInterval":1000,"AutoAcceptUntrustedCerts":false,"SessionTimeoutMs":45000} + """; + + var (config, isLegacy) = OpcUaEndpointConfigSerializer.Deserialize(legacyJson); + + Assert.True(isLegacy); + Assert.Equal("opc.tcp://scadalink-opcua:50000", config.EndpointUrl); + Assert.Equal(OpcUaSecurityMode.None, config.SecurityMode); + Assert.False(config.AutoAcceptUntrustedCerts); + Assert.Equal(45000, config.SessionTimeoutMs); + // publishInterval is not a recognized key (the real key is PublishingIntervalMs); + // FromFlatDict ignores unknown keys, so PublishingIntervalMs stays at its POCO default of 1000. + Assert.Equal(1000, config.PublishingIntervalMs); + } + [Theory] [InlineData("not json at all")] [InlineData("[1,2,3]")]