diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor
index b03337fd..aff7f627 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor
@@ -197,6 +197,15 @@
_error = null;
try
{
+ // Client-side per-driver config validation (the typed editor's Validate()), so a blank
+ // required field is caught here rather than silently saving and failing at deploy/connect.
+ var configError = TagConfigValidator.Validate(SelectedDriverType, _form.TagConfig);
+ if (configError is not null)
+ {
+ _error = configError;
+ return;
+ }
+
var input = new TagInput(
_form.TagId,
_form.Name,
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigValidator.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigValidator.cs
new file mode 100644
index 00000000..8e5f8722
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigValidator.cs
@@ -0,0 +1,29 @@
+namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
+
+///
+/// Client-side TagConfig validation dispatched by DriverType (parallel to
+/// ). Each entry parses the tag's config JSON into the driver's typed
+/// model and runs its Validate(). Returns an error string to block the TagModal save, or null
+/// when the config is valid — or when no typed validator is registered (unmapped drivers use the raw
+/// textarea and are validated server-side at deploy).
+///
+public static class TagConfigValidator
+{
+ private static readonly IReadOnlyDictionary> Validators =
+ new Dictionary>(StringComparer.OrdinalIgnoreCase)
+ {
+ ["ModbusTcp"] = j => ModbusTagConfigModel.FromJson(j).Validate(),
+ ["S7"] = j => S7TagConfigModel.FromJson(j).Validate(),
+ ["AbCip"] = j => AbCipTagConfigModel.FromJson(j).Validate(),
+ ["AbLegacy"] = j => AbLegacyTagConfigModel.FromJson(j).Validate(),
+ ["TwinCat"] = j => TwinCATTagConfigModel.FromJson(j).Validate(),
+ ["Focas"] = j => FocasTagConfigModel.FromJson(j).Validate(),
+ };
+
+ ///
+ /// Validates a tag's for the given .
+ /// Returns an error string to block save, or null when valid / no typed validator is registered.
+ ///
+ public static string? Validate(string? driverType, string? configJson)
+ => driverType is not null && Validators.TryGetValue(driverType, out var v) ? v(configJson) : null;
+}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigValidatorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigValidatorTests.cs
new file mode 100644
index 00000000..328b4f49
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigValidatorTests.cs
@@ -0,0 +1,49 @@
+using Shouldly;
+using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
+using Xunit;
+
+namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
+
+public sealed class TagConfigValidatorTests
+{
+ [Fact]
+ public void No_driver_type_is_valid()
+ => TagConfigValidator.Validate(null, "{}").ShouldBeNull();
+
+ [Fact]
+ public void Unmapped_driver_has_no_typed_validator_so_is_valid()
+ => TagConfigValidator.Validate("OpcUaClient", "{}").ShouldBeNull();
+
+ [Fact]
+ public void Modbus_has_no_required_field_so_empty_config_is_valid()
+ => TagConfigValidator.Validate("ModbusTcp", "{}").ShouldBeNull();
+
+ // Drivers whose Validate() requires a field: blank config must return an error string.
+ [Theory]
+ [InlineData("S7")]
+ [InlineData("AbCip")]
+ [InlineData("AbLegacy")]
+ [InlineData("TwinCat")]
+ [InlineData("Focas")]
+ public void Required_field_blank_is_rejected(string driverType)
+ {
+ TagConfigValidator.Validate(driverType, "{}").ShouldNotBeNullOrEmpty();
+ TagConfigValidator.Validate(driverType, null).ShouldNotBeNullOrEmpty();
+ }
+
+ [Fact]
+ public void S7_with_address_is_valid()
+ => TagConfigValidator.Validate("S7", """{"address":"DB1.DBW0"}""").ShouldBeNull();
+
+ [Fact]
+ public void AbCip_with_tag_path_is_valid()
+ => TagConfigValidator.Validate("AbCip", """{"tagPath":"Program:Main.Speed"}""").ShouldBeNull();
+
+ [Fact]
+ public void TwinCat_with_symbol_path_is_valid()
+ => TagConfigValidator.Validate("TwinCat", """{"symbolPath":"MAIN.bStart"}""").ShouldBeNull();
+
+ [Fact]
+ public void Driver_type_lookup_is_case_insensitive()
+ => TagConfigValidator.Validate("s7", "{}").ShouldNotBeNullOrEmpty();
+}