diff --git a/libraries/MTConnect.NET-JSON-cppagent/Streams/JsonConditions.cs b/libraries/MTConnect.NET-JSON-cppagent/Streams/JsonConditions.cs
index 8cfbb7f9..8adbbf71 100644
--- a/libraries/MTConnect.NET-JSON-cppagent/Streams/JsonConditions.cs
+++ b/libraries/MTConnect.NET-JSON-cppagent/Streams/JsonConditions.cs
@@ -9,8 +9,31 @@
namespace MTConnect.Streams.Json
{
+ ///
+ /// Typed representation of a Condition list on a Component stream,
+ /// bucketed by Condition level (Fault, Warning, Normal, Unavailable).
+ ///
+ ///
+ /// Serialized via in the
+ /// cppagent JSON v2 wire shape: an array of single-key wrapper
+ /// objects, one per Condition entry
+ /// (e.g. [{"Normal": {...}}, {"Warning": {...}}]).
+ /// The level order on the wire is fixed at Fault, Warning, Normal,
+ /// Unavailable; mixed-level interleaving is not round-trip preserved
+ /// through this typed model. The legacy MTConnect JSON v1
+ /// object-keyed shape ({"Fault": [...], "Warning": [...], ...})
+ /// is still accepted on the read path for back-compat.
+ ///
+ [System.Text.Json.Serialization.JsonConverter(typeof(JsonConditionsConverter))]
public class JsonConditions
{
+ ///
+ /// Materializes every level bucket into a flat list of
+ /// instances, tagged with the
+ /// corresponding . Enumeration order
+ /// matches the wire-emission order: Fault, then Warning, then
+ /// Normal, then Unavailable.
+ ///
[JsonIgnore]
public List Observations
{
@@ -42,15 +65,39 @@ public List Observations
}
}
+ ///
+ /// Condition entries at FAULT level. Source order is
+ /// preserved within the bucket; entries are emitted on the wire
+ /// as {"Fault": {...}} wrapper objects, ahead of every
+ /// other level.
+ ///
[JsonPropertyName("Fault")]
public IEnumerable Fault { get; set; }
+ ///
+ /// Condition entries at WARNING level. Source order is
+ /// preserved within the bucket; entries are emitted on the wire
+ /// as {"Warning": {...}} wrapper objects, after Fault
+ /// and before Normal.
+ ///
[JsonPropertyName("Warning")]
public IEnumerable Warning { get; set; }
+ ///
+ /// Condition entries at NORMAL level. Source order is
+ /// preserved within the bucket; entries are emitted on the wire
+ /// as {"Normal": {...}} wrapper objects, after Warning
+ /// and before Unavailable.
+ ///
[JsonPropertyName("Normal")]
public IEnumerable Normal { get; set; }
+ ///
+ /// Condition entries at UNAVAILABLE level. Source order
+ /// is preserved within the bucket; entries are emitted on the
+ /// wire as {"Unavailable": {...}} wrapper objects, after
+ /// every other level.
+ ///
[JsonPropertyName("Unavailable")]
public IEnumerable Unavailable { get; set; }
@@ -113,5 +160,5 @@ public JsonConditions(IEnumerable observations)
}
}
}
- }
-}
\ No newline at end of file
+ }
+}
diff --git a/libraries/MTConnect.NET-JSON-cppagent/Streams/JsonConditionsConverter.cs b/libraries/MTConnect.NET-JSON-cppagent/Streams/JsonConditionsConverter.cs
new file mode 100644
index 00000000..668d6ae7
--- /dev/null
+++ b/libraries/MTConnect.NET-JSON-cppagent/Streams/JsonConditionsConverter.cs
@@ -0,0 +1,208 @@
+// Copyright (c) 2023 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MTConnect.Streams.Json
+{
+ // Serializes JsonConditions in the cppagent JSON v2 wire shape: an
+ // array of single-key wrapper objects, one per Condition entry
+ // (e.g. [{"Normal": {...}}, {"Warning": {...}}]). The XSD
+ // ConditionListType is
+ // of Normal|Warning|Fault|Unavailable.
+ //
+ // Ordering: the typed JsonConditions POCO buckets entries by level
+ // (Fault, Warning, Normal, Unavailable). The Write path always emits
+ // in that fixed level order (Fault first, then Warning, then Normal,
+ // then Unavailable), with source order preserved within each bucket.
+ // Mixed-level interleaving on the wire is therefore NOT round-trip
+ // preserved: reading [{Fault:f1},{Normal:n1},{Fault:f2}] yields
+ // Fault=[f1,f2], Normal=[n1] and re-serializes as
+ // [{Fault:f1},{Fault:f2},{Normal:n1}]. Round-trip byte-identity
+ // holds only when each level's entries are already contiguous on
+ // the input wire.
+ //
+ // The legacy MTConnect JSON v1 object-keyed shape
+ // ({"Fault": [...], "Warning": [...], ...}) is still accepted on the
+ // read path for back-compat.
+ internal sealed class JsonConditionsConverter : JsonConverter
+ {
+ private const string FaultLevel = "Fault";
+ private const string WarningLevel = "Warning";
+ private const string NormalLevel = "Normal";
+ private const string UnavailableLevel = "Unavailable";
+
+ public override JsonConditions Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.Null:
+ return null;
+
+ case JsonTokenType.StartArray:
+ return ReadArrayShape(ref reader, options);
+
+ case JsonTokenType.StartObject:
+ return ReadObjectShape(ref reader, options);
+
+ default:
+ throw new JsonException(
+ $"Unexpected token '{reader.TokenType}' when reading JsonConditions; expected array, object, or null.");
+ }
+ }
+
+ public override void Write(Utf8JsonWriter writer, JsonConditions value, JsonSerializerOptions options)
+ {
+ if (value == null)
+ {
+ writer.WriteNullValue();
+ return;
+ }
+
+ writer.WriteStartArray();
+
+ WriteLevel(writer, FaultLevel, value.Fault, options);
+ WriteLevel(writer, WarningLevel, value.Warning, options);
+ WriteLevel(writer, NormalLevel, value.Normal, options);
+ WriteLevel(writer, UnavailableLevel, value.Unavailable, options);
+
+ writer.WriteEndArray();
+ }
+
+ private static void WriteLevel(Utf8JsonWriter writer, string levelName, IEnumerable entries, JsonSerializerOptions options)
+ {
+ if (entries == null) return;
+
+ foreach (var entry in entries)
+ {
+ writer.WriteStartObject();
+ writer.WritePropertyName(levelName);
+ JsonSerializer.Serialize(writer, entry, options);
+ writer.WriteEndObject();
+ }
+ }
+
+ private static JsonConditions ReadArrayShape(ref Utf8JsonReader reader, JsonSerializerOptions options)
+ {
+ var faults = new List();
+ var warnings = new List();
+ var normals = new List();
+ var unavailables = new List();
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndArray) break;
+
+ if (reader.TokenType != JsonTokenType.StartObject)
+ {
+ throw new JsonException(
+ $"Unexpected token '{reader.TokenType}' inside JsonConditions array; expected object wrapper.");
+ }
+
+ if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName)
+ {
+ throw new JsonException("Expected property name inside JsonConditions wrapper object.");
+ }
+
+ var levelName = reader.GetString();
+ if (!reader.Read())
+ {
+ throw new JsonException("Expected value after Condition level name in JsonConditions wrapper object.");
+ }
+ var entry = JsonSerializer.Deserialize(ref reader, options);
+ if (entry == null)
+ {
+ throw new JsonException("Null Condition entry value in JsonConditions wrapper.");
+ }
+
+ if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
+ {
+ throw new JsonException("Expected end of JsonConditions wrapper object after entry.");
+ }
+
+ switch (levelName)
+ {
+ case FaultLevel:
+ faults.Add(entry);
+ break;
+ case WarningLevel:
+ warnings.Add(entry);
+ break;
+ case NormalLevel:
+ normals.Add(entry);
+ break;
+ case UnavailableLevel:
+ unavailables.Add(entry);
+ break;
+ default:
+ throw new JsonException(
+ $"Unknown Condition level '{levelName}' in JsonConditions array; expected Fault, Warning, Normal, or Unavailable.");
+ }
+ }
+
+ return new JsonConditions
+ {
+ Fault = faults.Count > 0 ? faults : null,
+ Warning = warnings.Count > 0 ? warnings : null,
+ Normal = normals.Count > 0 ? normals : null,
+ Unavailable = unavailables.Count > 0 ? unavailables : null,
+ };
+ }
+
+ // Reads the legacy MTConnect JSON v1 object-keyed shape:
+ // {"Fault": [...], "Warning": [...], ...}. Duplicate level keys
+ // on the input (e.g. {"Fault":[a],"Fault":[b]}) are by-design
+ // last-write-wins: each occurrence overwrites the previous
+ // entry list. This is accepted asymmetry with the array path,
+ // which appends across all occurrences. Legacy producers that
+ // emit each level key exactly once are unaffected; the asymmetry
+ // only surfaces on malformed-or-duplicate legacy input, where
+ // last-write-wins is a deterministic, documented behavior.
+ private static JsonConditions ReadObjectShape(ref Utf8JsonReader reader, JsonSerializerOptions options)
+ {
+ var result = new JsonConditions();
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndObject) break;
+
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ throw new JsonException(
+ $"Unexpected token '{reader.TokenType}' inside JsonConditions object; expected property name.");
+ }
+
+ var levelName = reader.GetString();
+ if (!reader.Read())
+ {
+ throw new JsonException("Expected value after Condition level name in JsonConditions wrapper object.");
+ }
+ var entries = JsonSerializer.Deserialize>(ref reader, options);
+
+ switch (levelName)
+ {
+ case FaultLevel:
+ result.Fault = entries;
+ break;
+ case WarningLevel:
+ result.Warning = entries;
+ break;
+ case NormalLevel:
+ result.Normal = entries;
+ break;
+ case UnavailableLevel:
+ result.Unavailable = entries;
+ break;
+ default:
+ throw new JsonException(
+ $"Unknown Condition level '{levelName}' in JsonConditions object; expected Fault, Warning, Normal, or Unavailable.");
+ }
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-JSON-cppagent-Tests/Streams/JsonConditionsArrayShapeTests.cs b/tests/MTConnect.NET-JSON-cppagent-Tests/Streams/JsonConditionsArrayShapeTests.cs
new file mode 100644
index 00000000..5c982d9f
--- /dev/null
+++ b/tests/MTConnect.NET-JSON-cppagent-Tests/Streams/JsonConditionsArrayShapeTests.cs
@@ -0,0 +1,382 @@
+// Copyright (c) 2023 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using MTConnect.Streams.Json;
+using NUnit.Framework;
+using System.Collections.Generic;
+using System.Text.Json;
+
+namespace MTConnect.NET_JSON_cppagent_Tests.Streams
+{
+ // Pins the cppagent JSON v2 array-of-wrappers wire shape for
+ // ConditionListType. The XSD declares ConditionListType as
+ //
+ // of Normal|Warning|Fault|Unavailable; cppagent v2 emits one
+ // single-key wrapper object per entry.
+ //
+ // Ordering: the typed JsonConditions POCO buckets entries by level
+ // (Fault, Warning, Normal, Unavailable). The converter emits in
+ // that fixed level order, with source order preserved within each
+ // bucket. Mixed-level interleaving on the wire is therefore NOT
+ // round-trip preserved through the typed model — see the
+ // Read_ArrayShape_MixedLevelInterleaving_BucketsByLevel test for
+ // the explicit pin.
+ //
+ // Sources:
+ // - XSD: https://schemas.mtconnect.org/schemas/MTConnectStreams_2.7.xsd
+ // (complex type ConditionListType).
+ // - Prose: MTConnect Standard Part 2 section 13 "Condition".
+ // - cppagent reference (v2.7.0.7): printer/json_printer.cpp
+ // function print_condition.
+ [TestFixture]
+ public class JsonConditionsArrayShapeTests
+ {
+ private static JsonCondition MakeEntry(string dataItemId, string type)
+ {
+ return new JsonCondition
+ {
+ DataItemId = dataItemId,
+ Type = type,
+ };
+ }
+
+ private static JsonSerializerOptions Options() => new JsonSerializerOptions();
+
+ private static string FirstPropertyName(JsonElement element)
+ {
+ using var enumerator = element.EnumerateObject();
+ enumerator.MoveNext();
+ return enumerator.Current.Name;
+ }
+
+ // Case 1 — empty conditions serialize as the array shape, not an object shape.
+ [Test]
+ public void Write_EmptyConditions_EmitsEmptyArray()
+ {
+ var conditions = new JsonConditions();
+
+ var json = JsonSerializer.Serialize(conditions, Options());
+
+ Assert.That(json, Is.EqualTo("[]"));
+ }
+
+ // Case 2 — one Normal entry produces a 1-element array with a Normal wrapper.
+ [Test]
+ public void Write_SingleNormal_EmitsOneNormalWrapper()
+ {
+ var conditions = new JsonConditions
+ {
+ Normal = new List { MakeEntry("n1", "TEMPERATURE") },
+ };
+
+ var json = JsonSerializer.Serialize(conditions, Options());
+
+ using var doc = JsonDocument.Parse(json);
+ var root = doc.RootElement;
+ Assert.That(root.ValueKind, Is.EqualTo(JsonValueKind.Array));
+ Assert.That(root.GetArrayLength(), Is.EqualTo(1));
+
+ var wrapper = root[0];
+ Assert.That(wrapper.ValueKind, Is.EqualTo(JsonValueKind.Object));
+ Assert.That(wrapper.TryGetProperty("Normal", out var entry), Is.True);
+ Assert.That(entry.GetProperty("dataItemId").GetString(), Is.EqualTo("n1"));
+ }
+
+ // Case 3 — Fault + Warning emit in Fault, Warning order per the converter.
+ [Test]
+ public void Write_FaultThenWarning_EmitsInDeclaredEnumerationOrder()
+ {
+ var conditions = new JsonConditions
+ {
+ Fault = new List { MakeEntry("f1", "TEMPERATURE") },
+ Warning = new List { MakeEntry("w1", "POSITION") },
+ };
+
+ var json = JsonSerializer.Serialize(conditions, Options());
+
+ using var doc = JsonDocument.Parse(json);
+ var root = doc.RootElement;
+ Assert.That(root.GetArrayLength(), Is.EqualTo(2));
+
+ Assert.That(FirstPropertyName(root[0]), Is.EqualTo("Fault"));
+ Assert.That(FirstPropertyName(root[1]), Is.EqualTo("Warning"));
+ }
+
+ // Case 4 — all four levels populated emit in Fault, Warning, Normal, Unavailable order.
+ [Test]
+ public void Write_AllFourLevels_EmitsInFaultWarningNormalUnavailableOrder()
+ {
+ var conditions = new JsonConditions
+ {
+ Fault = new List { MakeEntry("f1", "TEMPERATURE") },
+ Warning = new List { MakeEntry("w1", "POSITION") },
+ Normal = new List { MakeEntry("n1", "AVAILABILITY") },
+ Unavailable = new List { MakeEntry("u1", "ROTATION") },
+ };
+
+ var json = JsonSerializer.Serialize(conditions, Options());
+
+ using var doc = JsonDocument.Parse(json);
+ var root = doc.RootElement;
+ Assert.That(root.GetArrayLength(), Is.EqualTo(4));
+
+ var keys = new List();
+ for (var i = 0; i < root.GetArrayLength(); i++)
+ {
+ foreach (var prop in root[i].EnumerateObject())
+ {
+ keys.Add(prop.Name);
+ }
+ }
+
+ Assert.That(keys, Is.EqualTo(new[] { "Fault", "Warning", "Normal", "Unavailable" }));
+ }
+
+ // Case 5 — multiple entries on one level produce one wrapper each in source order.
+ [Test]
+ public void Write_MultipleFaults_EmitsOneWrapperPerEntry()
+ {
+ var conditions = new JsonConditions
+ {
+ Fault = new List
+ {
+ MakeEntry("f1", "TEMPERATURE"),
+ MakeEntry("f2", "POSITION"),
+ MakeEntry("f3", "AVAILABILITY"),
+ },
+ };
+
+ var json = JsonSerializer.Serialize(conditions, Options());
+
+ using var doc = JsonDocument.Parse(json);
+ var root = doc.RootElement;
+ Assert.That(root.GetArrayLength(), Is.EqualTo(3));
+
+ var ids = new List();
+ foreach (var element in root.EnumerateArray())
+ {
+ Assert.That(element.TryGetProperty("Fault", out var entry), Is.True);
+ ids.Add(entry.GetProperty("dataItemId").GetString()!);
+ }
+
+ Assert.That(ids, Is.EqualTo(new[] { "f1", "f2", "f3" }));
+ }
+
+ // Case 5b — mixed-level interleaving on the input wire is bucketed
+ // by level on read and re-emitted in level order (Fault, Warning,
+ // Normal, Unavailable) on write. Pins the documented non-byte-
+ // identical round-trip for interleaved input — see the type
+ // comment on JsonConditionsConverter for the design rationale.
+ [Test]
+ public void Read_ArrayShape_MixedLevelInterleaving_BucketsByLevel()
+ {
+ const string interleaved =
+ "[{\"Fault\":{\"dataItemId\":\"f1\"}}," +
+ "{\"Normal\":{\"dataItemId\":\"n1\"}}," +
+ "{\"Fault\":{\"dataItemId\":\"f2\"}}]";
+
+ var parsed = JsonSerializer.Deserialize(interleaved, Options());
+
+ Assert.That(parsed, Is.Not.Null);
+ Assert.That(parsed!.Fault, Is.Not.Null);
+ Assert.That(parsed.Normal, Is.Not.Null);
+ Assert.That(parsed.Warning, Is.Null);
+ Assert.That(parsed.Unavailable, Is.Null);
+
+ var faultIds = new List();
+ foreach (var entry in parsed.Fault!) faultIds.Add(entry.DataItemId);
+ Assert.That(faultIds, Is.EqualTo(new[] { "f1", "f2" }));
+
+ var normalIds = new List();
+ foreach (var entry in parsed.Normal!) normalIds.Add(entry.DataItemId);
+ Assert.That(normalIds, Is.EqualTo(new[] { "n1" }));
+
+ var rewritten = JsonSerializer.Serialize(parsed, Options());
+ using var rewrittenDoc = JsonDocument.Parse(rewritten);
+ var rewrittenRoot = rewrittenDoc.RootElement;
+ Assert.That(rewrittenRoot.ValueKind, Is.EqualTo(JsonValueKind.Array));
+ Assert.That(rewrittenRoot.GetArrayLength(), Is.EqualTo(3));
+
+ var rewrittenKeys = new List();
+ var rewrittenDataItemIds = new List();
+ for (var i = 0; i < rewrittenRoot.GetArrayLength(); i++)
+ {
+ foreach (var prop in rewrittenRoot[i].EnumerateObject())
+ {
+ rewrittenKeys.Add(prop.Name);
+ rewrittenDataItemIds.Add(prop.Value.GetProperty("dataItemId").GetString()!);
+ }
+ }
+
+ Assert.That(rewrittenKeys, Is.EqualTo(new[] { "Fault", "Fault", "Normal" }));
+ Assert.That(rewrittenDataItemIds, Is.EqualTo(new[] { "f1", "f2", "n1" }));
+ }
+
+ // Case 6 — array JSON round-trips through Deserialize/Serialize without drift.
+ [Test]
+ public void RoundTrip_ArrayShape_IsByteIdenticalModuloWhitespace()
+ {
+ var original = new JsonConditions
+ {
+ Fault = new List { MakeEntry("f1", "TEMPERATURE") },
+ Warning = new List { MakeEntry("w1", "POSITION") },
+ Normal = new List { MakeEntry("n1", "AVAILABILITY") },
+ Unavailable = new List { MakeEntry("u1", "ROTATION") },
+ };
+
+ var json = JsonSerializer.Serialize(original, Options());
+ var parsed = JsonSerializer.Deserialize(json, Options());
+ var json2 = JsonSerializer.Serialize(parsed, Options());
+
+ Assert.That(json2, Is.EqualTo(json));
+ }
+
+ // Case 7 — legacy MTConnect JSON v1 object-keyed shape parses into the typed POCO.
+ [Test]
+ public void Read_LegacyObjectShape_PopulatesTypedProperties()
+ {
+ const string legacy =
+ "{\"Normal\":[{\"dataItemId\":\"n1\",\"type\":\"TEMPERATURE\"}]," +
+ "\"Fault\":[{\"dataItemId\":\"f1\",\"type\":\"POSITION\"}]}";
+
+ var parsed = JsonSerializer.Deserialize(legacy, Options());
+
+ Assert.That(parsed, Is.Not.Null);
+ Assert.That(parsed!.Normal, Is.Not.Null);
+ Assert.That(parsed.Fault, Is.Not.Null);
+
+ using var normalEnumerator = parsed.Normal!.GetEnumerator();
+ Assert.That(normalEnumerator.MoveNext(), Is.True);
+ Assert.That(normalEnumerator.Current.DataItemId, Is.EqualTo("n1"));
+
+ using var faultEnumerator = parsed.Fault!.GetEnumerator();
+ Assert.That(faultEnumerator.MoveNext(), Is.True);
+ Assert.That(faultEnumerator.Current.DataItemId, Is.EqualTo("f1"));
+ }
+
+ // Case 8 — null write emits "null" and round-trips back to a null reference.
+ [Test]
+ public void Null_WriteAndRead_RoundTripsToNull()
+ {
+ var json = JsonSerializer.Serialize(null!, Options());
+ Assert.That(json, Is.EqualTo("null"));
+
+ var parsed = JsonSerializer.Deserialize("null", Options());
+ Assert.That(parsed, Is.Null);
+ }
+
+ // Case 9 — invalid root token (number) raises JsonException with a recognisable message.
+ [Test]
+ public void Read_InvalidRootToken_ThrowsJsonException()
+ {
+ var ex = Assert.Throws(() =>
+ JsonSerializer.Deserialize("123", Options()));
+ Assert.That(ex, Is.Not.Null);
+ Assert.That(ex!.Message, Does.Contain("Unexpected token"));
+ }
+
+ // Coverage filler — the array-shape read path also handles all four levels
+ // and rejects unknown level names + malformed wrapper objects.
+ [Test]
+ public void Read_ArrayShape_PopulatesAllFourLevels()
+ {
+ const string json =
+ "[{\"Fault\":{\"dataItemId\":\"f1\"}}," +
+ "{\"Warning\":{\"dataItemId\":\"w1\"}}," +
+ "{\"Normal\":{\"dataItemId\":\"n1\"}}," +
+ "{\"Unavailable\":{\"dataItemId\":\"u1\"}}]";
+
+ var parsed = JsonSerializer.Deserialize(json, Options());
+
+ Assert.That(parsed, Is.Not.Null);
+ Assert.That(parsed!.Fault, Is.Not.Null);
+ Assert.That(parsed.Warning, Is.Not.Null);
+ Assert.That(parsed.Normal, Is.Not.Null);
+ Assert.That(parsed.Unavailable, Is.Not.Null);
+ }
+
+ [Test]
+ public void Read_ArrayShape_UnknownLevel_ThrowsJsonException()
+ {
+ const string json = "[{\"Bogus\":{\"dataItemId\":\"x1\"}}]";
+
+ var ex = Assert.Throws(() =>
+ JsonSerializer.Deserialize(json, Options()));
+ Assert.That(ex, Is.Not.Null);
+ Assert.That(ex!.Message, Does.Contain("Unknown Condition level"));
+ }
+
+ [Test]
+ public void Read_ArrayShape_NonObjectElement_ThrowsJsonException()
+ {
+ const string json = "[42]";
+
+ var ex = Assert.Throws(() =>
+ JsonSerializer.Deserialize(json, Options()));
+ Assert.That(ex, Is.Not.Null);
+ Assert.That(ex!.Message, Does.Contain("expected object wrapper"));
+ }
+
+ [Test]
+ public void Read_ArrayShape_WrapperWithoutPropertyName_ThrowsJsonException()
+ {
+ const string json = "[{}]";
+
+ var ex = Assert.Throws(() =>
+ JsonSerializer.Deserialize(json, Options()));
+ Assert.That(ex, Is.Not.Null);
+ Assert.That(ex!.Message, Does.Contain("Expected property name"));
+ }
+
+ [Test]
+ public void Read_ArrayShape_WrapperWithMultipleProperties_ThrowsJsonException()
+ {
+ const string json = "[{\"Fault\":{\"dataItemId\":\"f1\"},\"Warning\":{\"dataItemId\":\"w1\"}}]";
+
+ var ex = Assert.Throws(() =>
+ JsonSerializer.Deserialize(json, Options()));
+ Assert.That(ex, Is.Not.Null);
+ Assert.That(ex!.Message, Does.Contain("end of JsonConditions wrapper"));
+ }
+
+ [Test]
+ public void Read_ArrayShape_NullEntry_ThrowsJsonException()
+ {
+ const string json = "[{\"Normal\":null}]";
+
+ var ex = Assert.Throws(() =>
+ JsonSerializer.Deserialize(json, Options()));
+ Assert.That(ex, Is.Not.Null);
+ Assert.That(ex!.Message, Does.Contain("Null Condition entry"));
+ }
+
+ [Test]
+ public void Read_ObjectShape_UnknownLevel_ThrowsJsonException()
+ {
+ const string json = "{\"Bogus\":[{\"dataItemId\":\"x1\"}]}";
+
+ var ex = Assert.Throws(() =>
+ JsonSerializer.Deserialize(json, Options()));
+ Assert.That(ex, Is.Not.Null);
+ Assert.That(ex!.Message, Does.Contain("Unknown Condition level"));
+ }
+
+ [Test]
+ public void Read_ObjectShape_PopulatesAllFourLevels()
+ {
+ const string json =
+ "{\"Fault\":[{\"dataItemId\":\"f1\"}]," +
+ "\"Warning\":[{\"dataItemId\":\"w1\"}]," +
+ "\"Normal\":[{\"dataItemId\":\"n1\"}]," +
+ "\"Unavailable\":[{\"dataItemId\":\"u1\"}]}";
+
+ var parsed = JsonSerializer.Deserialize(json, Options());
+
+ Assert.That(parsed, Is.Not.Null);
+ Assert.That(parsed!.Fault, Is.Not.Null);
+ Assert.That(parsed.Warning, Is.Not.Null);
+ Assert.That(parsed.Normal, Is.Not.Null);
+ Assert.That(parsed.Unavailable, Is.Not.Null);
+ }
+ }
+}