Skip to content

JSON-cppagent-mqtt formatter emits Condition in legacy v1 object shape (not cppagent v2 array of wrappers) #154

@ottobolyos

Description

@ottobolyos

Summary

MTConnect.NET-JSON-cppagent's JsonConditions class is a typed POCO with four named array-of-JsonCondition properties — Fault, Warning, Normal, Unavailable — each tagged with [JsonPropertyName(...)]. When serialised through System.Text.Json, that shape lands on the wire as:

"Condition": {
    "Fault":       [ ],
    "Warning":     [ ],
    "Normal":      [ ],
    "Unavailable": [ ]
}

— the legacy MTConnect JSON v1 object-keyed-by-level shape. The cppagent JSON v2 wire form (which JSON-cppagent-mqtt formatter is named after) is an array of single-key wrapper objects keyed by level name:

"Condition": [
    { "Normal":      { "dataItemId": "c1", } },
    { "Warning":     { "dataItemId": "c2", "type": "LOGIC_PROGRAM", "value": "check" } },
    { "Fault":       { "dataItemId": "c3", } },
    { "Unavailable": { "dataItemId": "c4", } }
]

Strict cppagent JSON v2 consumers (downstream bridges, dashboards) reject the v1 shape because the property keys collide with the per-level wrapper grammar — they expect to walk the array and dispatch on each wrapper's single key. The result is a wire envelope that is labelled JSON-cppagent-mqtt but is not cppagent-compatible for any observation that carries Conditions.

Reproduction

Boot a JSON-cppagent-mqtt agent, publish any Condition observation, subscribe to MTConnect/Current/+, and inspect the resulting envelope:

{ "MTConnectStreams": { "Streams": { "DeviceStream": [ { "ComponentStream": [ {
  "Condition": {
    "Normal": [ { "dataItemId": "cond-1", "sequence": 42 } ]
  }
} ] } ] } } }

vs. cppagent v2.7.0.7 in an identical configuration:

{ "MTConnectStreams": { "Streams": { "DeviceStream": [ { "ComponentStream": [ {
  "Condition": [
    { "Normal": { "dataItemId": "cond-1", "sequence": 42 } }
  ]
} ] } ] } } }

Root cause

libraries/MTConnect.NET-JSON-cppagent/Streams/JsonConditions.cs:

public class JsonConditions
{
    [JsonPropertyName("Fault")]
    public IEnumerable<JsonCondition> Fault { get; set; }

    [JsonPropertyName("Warning")]
    public IEnumerable<JsonCondition> Warning { get; set; }

    [JsonPropertyName("Normal")]
    public IEnumerable<JsonCondition> Normal { get; set; }

    [JsonPropertyName("Unavailable")]
    public IEnumerable<JsonCondition> Unavailable { get; set; }
    ...
}

JsonComponentStream declares the property as:

[JsonPropertyName("Condition")]
public JsonConditions Conditions { get; set; }

Default System.Text.Json serialisation of a typed object with four properties produces the v1 object-keyed shape directly. There is no converter, no array projection.

Authority

XSD — MTConnectStreams_2.7.xsd, ConditionListType (verbatim)

<xs:complexType name='ConditionListType'>
    <xs:sequence>
        <xs:choice minOccurs='0' maxOccurs='unbounded'>
            <xs:element name='Normal'      type='ConditionType'/>
            <xs:element name='Warning'     type='ConditionType'/>
            <xs:element name='Fault'       type='ConditionType'/>
            <xs:element name='Unavailable' type='ConditionType'/>
        </xs:choice>
    </xs:sequence>
</xs:complexType>

XML wire form is unambiguously a sequence (ordered list) of Normal | Warning | Fault | Unavailable elements — i.e. interleaved per observation, in observation order. The JSON v2 array-of-wrappers shape is the natural translation; the v1 object-keyed shape is an early MTConnect approximation that loses observation ordering.

Prose — MTConnect Standard Part 2, §13 "Condition"

A Condition observation is a discrete state transition belonging to one of the four levels. Each transition is a separate observation; the wire form must preserve the per-level dispatch + the per-observation identity (dataItemId, sequence, timestamp). The v1 object-keyed shape can only carry "all Normal", "all Warning", etc. — it cannot interleave levels in observation order, which is what cppagent's array form delivers.

cppagent reference (JSON v2 wire-shape reference implementation, NOT a spec source)

cppagent v2.7.0.7's printer/json_printer.cpp and the parity test fixtures in test_package/json_printer_stream_test.cpp (ConditionDataItem test cases) emit the array shape for every Condition observation, regardless of level distribution. The unit-test corpus in cppagent — which has been stable across v2.x — never produces the object-keyed shape that MT.NET emits.

Proposed fix

Add a JsonConverter<JsonConditions> that on Write projects the four named properties into an array of {Level: jsonCondition} wrappers, and on Read accepts both shapes for back-compat. Apply via [JsonConverter(typeof(JsonConditionsConverter))] on the JsonConditions class.

Impact

Any cppagent JSON v2 consumer (downstream bridge, dashboard, historian) that walks the Condition array and dispatches on each wrapper's single key — i.e. every consumer that follows the cppagent reference shape — fails on every Current envelope carrying a Condition emitted by JSON-cppagent-mqtt.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions