diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Serialization/MxGatewayEndpointConfigSerializer.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Serialization/MxGatewayEndpointConfigSerializer.cs
new file mode 100644
index 00000000..40282828
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Serialization/MxGatewayEndpointConfigSerializer.cs
@@ -0,0 +1,65 @@
+using System.Globalization;
+using System.Text.Json;
+using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections;
+
+namespace ZB.MOM.WW.ScadaBridge.Commons.Serialization;
+
+///
+/// Serializes to/from the typed JSON stored in
+/// DataConnection.PrimaryConfiguration / BackupConfiguration, and flattens
+/// it to the IDictionary<string,string> shape IDataConnection.ConnectAsync
+/// expects. MxGateway is net-new, so there is no legacy shape to recover — a row that
+/// fails to parse yields a default config.
+///
+public static class MxGatewayEndpointConfigSerializer
+{
+ private static readonly JsonSerializerOptions JsonOpts = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = false,
+ };
+
+ /// Serializes a config to the typed JSON shape.
+ /// The endpoint configuration to serialize.
+ public static string Serialize(MxGatewayEndpointConfig config)
+ => JsonSerializer.Serialize(config, JsonOpts);
+
+ /// Parses stored config JSON; null/blank/malformed yields a default config.
+ /// The stored JSON string.
+ public static MxGatewayEndpointConfig Deserialize(string? json)
+ {
+ if (string.IsNullOrWhiteSpace(json)) return new MxGatewayEndpointConfig();
+ try { return JsonSerializer.Deserialize(json, JsonOpts) ?? new MxGatewayEndpointConfig(); }
+ catch (JsonException) { return new MxGatewayEndpointConfig(); }
+ }
+
+ /// Flattens the typed config to the key-value shape the adapter consumes.
+ /// The endpoint configuration to flatten.
+ public static IDictionary ToFlatDict(MxGatewayEndpointConfig c) => new Dictionary
+ {
+ ["Endpoint"] = c.Endpoint,
+ ["ApiKey"] = c.ApiKey,
+ ["ClientName"] = c.ClientName,
+ ["WriteUserId"] = c.WriteUserId.ToString(CultureInfo.InvariantCulture),
+ ["UseTls"] = c.UseTls.ToString(),
+ ["CaFile"] = c.CaFile,
+ ["ServerName"] = c.ServerName,
+ ["ReadTimeoutMs"] = c.ReadTimeoutMs.ToString(CultureInfo.InvariantCulture),
+ };
+
+ /// Reconstructs a config from the flat key-value shape; invalid numerics fall back to defaults.
+ /// The flat dictionary.
+ public static MxGatewayEndpointConfig FromFlatDict(IDictionary d)
+ {
+ var c = new MxGatewayEndpointConfig();
+ if (d.TryGetValue("Endpoint", out var ep) && !string.IsNullOrWhiteSpace(ep)) c.Endpoint = ep;
+ if (d.TryGetValue("ApiKey", out var ak)) c.ApiKey = ak;
+ if (d.TryGetValue("ClientName", out var cn)) c.ClientName = cn;
+ if (d.TryGetValue("WriteUserId", out var wu) && int.TryParse(wu, out var wuv)) c.WriteUserId = wuv;
+ if (d.TryGetValue("UseTls", out var tls) && bool.TryParse(tls, out var tlsv)) c.UseTls = tlsv;
+ if (d.TryGetValue("CaFile", out var ca)) c.CaFile = ca;
+ if (d.TryGetValue("ServerName", out var sn)) c.ServerName = sn;
+ if (d.TryGetValue("ReadTimeoutMs", out var rt) && int.TryParse(rt, out var rtv)) c.ReadTimeoutMs = rtv;
+ return c;
+ }
+}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/DataConnections/MxGatewayEndpointConfigSerializerTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/DataConnections/MxGatewayEndpointConfigSerializerTests.cs
new file mode 100644
index 00000000..2493111c
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/DataConnections/MxGatewayEndpointConfigSerializerTests.cs
@@ -0,0 +1,84 @@
+using ZB.MOM.WW.ScadaBridge.Commons.Serialization;
+using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections;
+
+namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.DataConnections;
+
+public class MxGatewayEndpointConfigSerializerTests
+{
+ [Fact]
+ public void Serialize_then_Deserialize_round_trips_all_fields()
+ {
+ var original = new MxGatewayEndpointConfig
+ {
+ Endpoint = "https://gw:5001",
+ ApiKey = "secret-key",
+ ClientName = "client-a",
+ WriteUserId = 7,
+ UseTls = true,
+ CaFile = "/certs/ca.pem",
+ ServerName = "gw.local",
+ ReadTimeoutMs = 1234
+ };
+
+ var json = MxGatewayEndpointConfigSerializer.Serialize(original);
+ var round = MxGatewayEndpointConfigSerializer.Deserialize(json);
+
+ Assert.Equal(original.Endpoint, round.Endpoint);
+ Assert.Equal(original.ApiKey, round.ApiKey);
+ Assert.Equal(original.ClientName, round.ClientName);
+ Assert.Equal(original.WriteUserId, round.WriteUserId);
+ Assert.Equal(original.UseTls, round.UseTls);
+ Assert.Equal(original.CaFile, round.CaFile);
+ Assert.Equal(original.ServerName, round.ServerName);
+ Assert.Equal(original.ReadTimeoutMs, round.ReadTimeoutMs);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("{ not valid json")]
+ public void Deserialize_null_blank_or_malformed_returns_default(string? json)
+ {
+ var def = new MxGatewayEndpointConfig();
+ var result = MxGatewayEndpointConfigSerializer.Deserialize(json);
+ Assert.Equal(def.Endpoint, result.Endpoint);
+ Assert.Equal(def.ReadTimeoutMs, result.ReadTimeoutMs);
+ }
+
+ [Fact]
+ public void ToFlatDict_FromFlatDict_round_trips()
+ {
+ var original = new MxGatewayEndpointConfig
+ {
+ Endpoint = "http://x:5000",
+ ApiKey = "k",
+ ClientName = "c",
+ WriteUserId = 3,
+ UseTls = true,
+ CaFile = "/ca",
+ ServerName = "s",
+ ReadTimeoutMs = 999
+ };
+
+ var dict = MxGatewayEndpointConfigSerializer.ToFlatDict(original);
+ var round = MxGatewayEndpointConfigSerializer.FromFlatDict(dict);
+
+ Assert.Equal(original.Endpoint, round.Endpoint);
+ Assert.Equal(original.ApiKey, round.ApiKey);
+ Assert.Equal(original.ClientName, round.ClientName);
+ Assert.Equal(original.WriteUserId, round.WriteUserId);
+ Assert.Equal(original.UseTls, round.UseTls);
+ Assert.Equal(original.CaFile, round.CaFile);
+ Assert.Equal(original.ServerName, round.ServerName);
+ Assert.Equal(original.ReadTimeoutMs, round.ReadTimeoutMs);
+ }
+
+ [Fact]
+ public void FromFlatDict_invalid_numeric_falls_back_to_default()
+ {
+ var back = MxGatewayEndpointConfigSerializer.FromFlatDict(
+ new Dictionary { ["ReadTimeoutMs"] = "not-a-number" });
+ Assert.Equal(new MxGatewayEndpointConfig().ReadTimeoutMs, back.ReadTimeoutMs);
+ }
+}