diff --git a/Libraries/Opc.Ua.PubSub/Encoding/JsonDataSetMessage.cs b/Libraries/Opc.Ua.PubSub/Encoding/JsonDataSetMessage.cs
index b3918d5624..909056afcd 100644
--- a/Libraries/Opc.Ua.PubSub/Encoding/JsonDataSetMessage.cs
+++ b/Libraries/Opc.Ua.PubSub/Encoding/JsonDataSetMessage.cs
@@ -499,8 +499,12 @@ private void EncodeField(IJsonEncoder encoder, Field field)
Variant valueToEncode = field.Value.WrappedValue;
- // The StatusCode.Good value is not encoded correctly then it shall be committed
- if (valueToEncode == StatusCodes.Good &&
+ // Only treat an actual StatusCode value equal to Good as null to avoid misencoding
+ bool isStatusCodeValue =
+ valueToEncode.TypeInfo?.BuiltInType == BuiltInType.StatusCode ||
+ valueToEncode.Value is StatusCode;
+ if (isStatusCodeValue &&
+ valueToEncode == StatusCodes.Good &&
m_fieldTypeEncoding != FieldTypeEncodingMask.Variant)
{
valueToEncode = Variant.Null;
diff --git a/Tests/Opc.Ua.PubSub.Tests/Encoding/JsonDataSetMessageTests.cs b/Tests/Opc.Ua.PubSub.Tests/Encoding/JsonDataSetMessageTests.cs
new file mode 100644
index 0000000000..6627f5efb4
--- /dev/null
+++ b/Tests/Opc.Ua.PubSub.Tests/Encoding/JsonDataSetMessageTests.cs
@@ -0,0 +1,316 @@
+/* ========================================================================
+ * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
+ *
+ * OPC Foundation MIT License 1.00
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * The complete license agreement can be found here:
+ * http://opcfoundation.org/License/MIT/1.00/
+ * ======================================================================*/
+
+using System;
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using Opc.Ua;
+using Opc.Ua.PubSub.PublishedData;
+using Opc.Ua.Tests;
+using PubSubEncoding = Opc.Ua.PubSub.Encoding;
+
+namespace Opc.Ua.PubSub.Tests.Encoding
+{
+ ///
+ /// Tests for JsonDataSetMessage encoding behavior.
+ /// Validates correct handling of zero values vs StatusCode.Good per OPC UA Part 6 specification.
+ ///
+ /// Note: JsonDataSetMessage currently only supports Reversible and NonReversible encoding modes.
+ /// Compact and Verbose encoding modes are not yet supported for PubSub messages because
+ /// the encoder throws when trying to modify ForceNamespaceUri property with these modes.
+ ///
+ [TestFixture]
+ [Parallelizable]
+ public class JsonDataSetMessageTests
+ {
+ #region Regression Tests - UInt32 Zero Value Preservation (DataValue Mode)
+
+ ///
+ /// Regression test: UInt32 value of 0 must not be confused with StatusCode.Good
+ /// and must be preserved in DataValue mode with Reversible encoding.
+ ///
+ [Test]
+ public void EncodeUInt32ZeroPreservesValueInDataValueModeReversible()
+ {
+ var field = CreateField("TestField", BuiltInType.UInt32, (uint)0);
+ var message = CreateDataValueMessage(field);
+
+ var json = EncodeMessage(message, JsonEncodingType.Reversible);
+ var fieldObj = GetPayloadField(json, "TestField");
+
+ Assert.That(fieldObj, Is.Not.Null, "Field should be encoded.");
+ Assert.That(fieldObj["Value"]?.Value(), Is.EqualTo(0u),
+ "UInt32 zero value must be preserved in Reversible encoding.");
+ }
+
+ ///
+ /// Regression test: UInt32 value of 0 must not be confused with StatusCode.Good
+ /// and must be preserved in DataValue mode with NonReversible encoding.
+ ///
+ [Test]
+ public void EncodeUInt32ZeroPreservesValueInDataValueModeNonReversible()
+ {
+ var field = CreateField("TestField", BuiltInType.UInt32, (uint)0);
+ var message = CreateDataValueMessage(field);
+
+ var json = EncodeMessage(message, JsonEncodingType.NonReversible);
+ var fieldObj = GetPayloadField(json, "TestField");
+
+ Assert.That(fieldObj, Is.Not.Null, "Field should be encoded.");
+ Assert.That(fieldObj["Value"]?.Value(), Is.EqualTo(0u),
+ "UInt32 zero value must be preserved in NonReversible encoding.");
+ }
+
+ #endregion
+
+ #region Regression Tests - UInt32 Zero Value Preservation (RawData Mode)
+
+ ///
+ /// Regression test: UInt32 value of 0 must be preserved in RawData mode.
+ /// Per OPC 10000-6: RawData uses non-reversible encoding for the value itself.
+ ///
+ [Test]
+ public void EncodeUInt32ZeroPreservesValueInRawDataModeReversible()
+ {
+ var field = CreateField("TestField", BuiltInType.UInt32, (uint)0);
+
+ var message = new PubSubEncoding.JsonDataSetMessage(new DataSet { Fields = [field] });
+ message.SetFieldContentMask(DataSetFieldContentMask.RawData);
+
+ var json = EncodeMessage(message, JsonEncodingType.Reversible);
+ var root = JObject.Parse(json);
+ var payload = (root["Payload"] as JObject) ?? root;
+
+ Assert.That(payload["TestField"]?.Value(), Is.EqualTo(0u),
+ "UInt32 zero value must be preserved in RawData mode with Reversible encoding.");
+ }
+
+ ///
+ /// Regression test: UInt32 value of 0 must be preserved in RawData mode with NonReversible encoding.
+ ///
+ [Test]
+ public void EncodeUInt32ZeroPreservesValueInRawDataModeNonReversible()
+ {
+ var field = CreateField("TestField", BuiltInType.UInt32, (uint)0);
+
+ var message = new PubSubEncoding.JsonDataSetMessage(new DataSet { Fields = [field] });
+ message.SetFieldContentMask(DataSetFieldContentMask.RawData);
+
+ var json = EncodeMessage(message, JsonEncodingType.NonReversible);
+ var root = JObject.Parse(json);
+ var payload = (root["Payload"] as JObject) ?? root;
+
+ Assert.That(payload["TestField"]?.Value(), Is.EqualTo(0u),
+ "UInt32 zero value must be preserved in RawData mode with NonReversible encoding.");
+ }
+
+ #endregion
+
+ #region Regression Tests - UInt32 Zero Value Preservation (Variant Mode)
+
+ ///
+ /// In Variant mode (FieldContentMask.None), values are encoded with type information.
+ /// UInt32 zero should still be preserved as it's a valid value.
+ /// Per OPC 10000-6: Variant mode uses reversible encoding with Type/Body structure.
+ ///
+ [Test]
+ public void EncodeUInt32ZeroPreservesValueInVariantModeReversible()
+ {
+ var field = CreateField("TestField", BuiltInType.UInt32, (uint)0);
+
+ var message = new PubSubEncoding.JsonDataSetMessage(new DataSet { Fields = [field] });
+ message.SetFieldContentMask(DataSetFieldContentMask.None); // Variant mode
+
+ var json = EncodeMessage(message, JsonEncodingType.Reversible);
+ var root = JObject.Parse(json);
+ var payload = (root["Payload"] as JObject) ?? root;
+
+ // In Variant mode with Reversible encoding, format is { "Type": 7, "Body": 0 }
+ var variantObj = payload["TestField"] as JObject;
+ Assert.That(variantObj, Is.Not.Null, "Field should be encoded as Variant object.");
+ Assert.That(variantObj["Body"]?.Value(), Is.EqualTo(0u),
+ "UInt32 zero value must be preserved in Variant Body.");
+ }
+
+ #endregion
+
+ #region StatusCode.Good Encoding Tests
+
+ ///
+ /// Verify that a real StatusCode.Good value results in null/omitted Value
+ /// in DataValue mode per spec: "The Code is omitted if the numeric code is 0 (Good)."
+ ///
+ [Test]
+ public void EncodeStatusCodeGoodResultsInNullValueInDataValueModeReversible()
+ {
+ var field = CreateStatusCodeField("StatusField", StatusCodes.Good);
+ var message = CreateDataValueMessage(field);
+
+ var json = EncodeMessage(message, JsonEncodingType.Reversible);
+ var root = JObject.Parse(json);
+ var payload = (root["Payload"] as JObject) ?? root;
+
+ var fieldObj = payload["StatusField"] as JObject;
+ Assert.That(fieldObj, Is.Not.Null, "Field should be present.");
+
+ // The Value field should be omitted entirely (StatusCode.Good is intentionally nulled)
+ Assert.That(fieldObj["Value"], Is.Null,
+ "StatusCode.Good should result in omitted Value in Reversible DataValue mode.");
+ }
+
+ ///
+ /// Verify that a real StatusCode.Good value results in null/omitted Value
+ /// in DataValue mode with NonReversible encoding.
+ ///
+ [Test]
+ public void EncodeStatusCodeGoodResultsInNullValueInDataValueModeNonReversible()
+ {
+ var field = CreateStatusCodeField("StatusField", StatusCodes.Good);
+ var message = CreateDataValueMessage(field);
+
+ var json = EncodeMessage(message, JsonEncodingType.NonReversible);
+ var root = JObject.Parse(json);
+ var payload = (root["Payload"] as JObject) ?? root;
+
+ var fieldObj = payload["StatusField"] as JObject;
+ Assert.That(fieldObj, Is.Not.Null, "Field should be present.");
+
+ // The Value field should be omitted entirely (StatusCode.Good is intentionally nulled)
+ Assert.That(fieldObj["Value"], Is.Null,
+ "StatusCode.Good should result in omitted Value in NonReversible DataValue mode.");
+ }
+
+ ///
+ /// Verify that a non-Good StatusCode value is preserved in Reversible encoding.
+ ///
+ [Test]
+ public void EncodeStatusCodeBadPreservesValueReversible()
+ {
+ var field = CreateStatusCodeField("StatusField", StatusCodes.BadInvalidArgument);
+ var message = CreateDataValueMessage(field);
+
+ var json = EncodeMessage(message, JsonEncodingType.Reversible);
+ var root = JObject.Parse(json);
+ var payload = (root["Payload"] as JObject) ?? root;
+
+ var fieldObj = payload["StatusField"] as JObject;
+ Assert.That(fieldObj, Is.Not.Null, "Field should be present.");
+
+ // A bad StatusCode should be encoded
+ var valueToken = fieldObj["Value"];
+ Assert.That(valueToken, Is.Not.Null, "Bad StatusCode value should be present in Reversible encoding.");
+ }
+
+ ///
+ /// Verify that a non-Good StatusCode value is preserved in NonReversible encoding.
+ ///
+ [Test]
+ public void EncodeStatusCodeBadPreservesValueNonReversible()
+ {
+ var field = CreateStatusCodeField("StatusField", StatusCodes.BadInvalidArgument);
+ var message = CreateDataValueMessage(field);
+
+ var json = EncodeMessage(message, JsonEncodingType.NonReversible);
+ var root = JObject.Parse(json);
+ var payload = (root["Payload"] as JObject) ?? root;
+
+ var fieldObj = payload["StatusField"] as JObject;
+ Assert.That(fieldObj, Is.Not.Null, "Field should be present.");
+
+ // A bad StatusCode should be encoded
+ var valueToken = fieldObj["Value"];
+ Assert.That(valueToken, Is.Not.Null, "Bad StatusCode value should be present in NonReversible encoding.");
+ }
+
+ #endregion
+
+
+ #region Helper Methods
+
+ private static Field CreateField(string name, BuiltInType builtInType, object value)
+ {
+ return new Field
+ {
+ FieldMetaData = new FieldMetaData
+ {
+ Name = name,
+ BuiltInType = (byte)builtInType,
+ ValueRank = ValueRanks.Scalar
+ },
+ Value = new DataValue(new Variant(value))
+ {
+ SourceTimestamp = DateTime.UtcNow
+ }
+ };
+ }
+
+ private static Field CreateStatusCodeField(string name, uint statusCode)
+ {
+ return new Field
+ {
+ FieldMetaData = new FieldMetaData
+ {
+ Name = name,
+ BuiltInType = (byte)BuiltInType.StatusCode,
+ ValueRank = ValueRanks.Scalar
+ },
+ Value = new DataValue(new Variant(new StatusCode(statusCode)))
+ };
+ }
+
+ private static PubSubEncoding.JsonDataSetMessage CreateDataValueMessage(Field field)
+ {
+ var message = new PubSubEncoding.JsonDataSetMessage(new DataSet { Fields = [field] });
+ // DataValue mode requires at least one of these flags
+ message.SetFieldContentMask(
+ DataSetFieldContentMask.StatusCode |
+ DataSetFieldContentMask.SourceTimestamp);
+ return message;
+ }
+
+ private static string EncodeMessage(PubSubEncoding.JsonDataSetMessage message, JsonEncodingType encodingType)
+ {
+ ITelemetryContext telemetry = NUnitTelemetryContext.Create();
+ var encoder = new JsonEncoder(
+ new ServiceMessageContext(telemetry),
+ encodingType);
+ message.Encode(encoder);
+ return encoder.CloseAndReturnText();
+ }
+
+ private static JObject GetPayloadField(string json, string fieldName)
+ {
+ var root = JObject.Parse(json);
+ var payload = (root["Payload"] as JObject) ?? root;
+ return payload?[fieldName] as JObject;
+ }
+
+ #endregion
+ }
+}