Skip to content

JSON-cppagent-mqtt: numeric Sample value emitted as JSON string instead of number token #129

@ottobolyos

Description

@ottobolyos

Summary

Under the JSON-cppagent-mqtt document format, numeric Sample values are emitted as JSON string tokens ("value": "1586.66") instead of JSON number tokens ("value": 1586.66). This breaks every consumer coded against the cppagent reference's numeric-token output and is incompatible with the XSD type FloatSampleValueType, which is a union of xs:float and the "UNAVAILABLE" sentinel — a numeric wrapped in quotes matches neither member of that union.

Verified by reproduction — 2026-04-20

Reproduced live against trakhound/mtconnect.net-agent:6.9.0 with a Python SHDR source feeding <ts>|temp|<float> into a single TEMPERATURE Sample data item. MT.NET emitted:

"Temperature": [ { "value": "863.7060151979725", "dataItemId": "temp", ... } ]

Side-by-side, cppagent v2.7.0.7 in an identical configuration (same device, same SHDR feed, same broker) emitted:

"Temperature": [ { "value": 863.7070151979725, "dataItemId": "temp", ... } ]

Both agents correctly converted FAHRENHEITCELSIUS (the XML declares units="CELSIUS" nativeUnits="FAHRENHEIT"). The divergence is strictly the quoting of the numeric value: MT.NET writes a JSON string, cppagent writes a JSON number token.

Environment

  • MTConnect.NET-JSON-cppagent + MTConnect.NET-Applications-Agents at v6.9.0.2 (2025-10-16, latest).
  • Broker: eclipse-mosquitto:2.0.22.
  • Format ID: JSON-cppagent-mqtt.

Reproduction

applications/MTConnect-Agent/appsettings.yaml:

defaultVersion: 2.5.0.0
modules:
  - mqtt-relay:
      server: localhost
      port: 1883
      topicPrefix: MTConnect
      documentFormat: JSON-cppagent-mqtt
      sampleInterval: 500
  - shdr-adapter:
      device: TestDevice
      hostname: localhost
      port: 7878

devices.xml (one TEMPERATURE SAMPLE data item is enough):

<Device id="d1" name="TestDevice" uuid="UUID-TD1">
  <Components>
    <Heating id="h1" name="MainController">
      <DataItems>
        <DataItem id="temp" type="TEMPERATURE" category="SAMPLE" units="FAHRENHEIT"/>
      </DataItems>
    </Heating>
  </Components>
</Device>

Feed one numeric value via SHDR:

printf '%s|temp|1586.66\n' "$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)" | nc -q1 localhost 7878
mosquitto_sub -h localhost -t 'MTConnect/Sample/UUID-TD1' -C 1 | jq '.MTConnectStreams.Streams.DeviceStream[0].ComponentStream[0].Samples'

Observed

{
  "Temperature": [
    {
      "value": "1586.66",
      "dataItemId": "temp",
      "timestamp": "...",
      "sequence": 172
    }
  ]
}

Expected (cppagent v2 JSON reference shape; this issue is strictly about the string-quoted value)

{
  "Temperature": [
    {
      "value": 1586.66,
      "dataItemId": "temp",
      "timestamp": "...",
      "sequence": 172
    }
  ]
}

The surrounding object-of-arrays shape ("Samples": { "TypeName": [...] }) matches cppagent's JSON v2 output (see cppagent json_printer_stream_test.cppTEST_F(JsonPrinterStreamTest, samples_and_events_version_2) at the assertions stream.at("/Samples"_json_pointer).is_object() + samples.at("/PathPosition"_json_pointer).is_array()). The divergence this issue targets is the scalar-value token inside each observation: cppagent emits JSON numbers via .get<double>() (verified in the same test file at ASSERT_EQ(10.0, amp.at("/Amperage/value"_json_pointer).get<double>())); the MQTT relay emits the same value as a JSON string.

Authority

JSON is not part of the MTConnect normative standard, but the XSD does pin the semantic type that any JSON translation must preserve.

MTConnectStreams_2.7.xsd (identical declaration in _2.6.xsd and _2.5.xsd):

<xs:simpleType name='FloatSampleValueType'>
  <xs:annotation>
    <xs:documentation>Common floating point sample value</xs:documentation>
  </xs:annotation>
  <xs:union memberTypes='xs:float UnavailableValueType'/>
</xs:simpleType>

FloatSampleValueType is a union of xs:float and the "UNAVAILABLE" sentinel. A faithful JSON translation therefore emits:

  • a JSON number token when the value is numeric, or
  • the JSON string "UNAVAILABLE" when the value is unavailable.

Strings in JSON are legitimate only for:

  • the "UNAVAILABLE" sentinel;
  • EVENT observations whose values are xs:string or enumeration types (not xs:float);
  • DATA_SET / TABLE representation observations (which are JSON objects, not primitive strings).

A VALUE-representation numeric Sample wrapped in quotes matches neither member of FloatSampleValueType's union and is rejected by every consumer written against the cppagent JSON reference.

The cppagent reference implementation emits numeric Samples as JSON number tokens. The JSON-CPPAGENT-MQTT format-label is an explicit promise of cppagent-reference compatibility; diverging here breaks that contract.

Likely root cause

The value-serialisation path appears to route through .ToString() + a string-token write, presumably to avoid floating-point precision loss in .NET's default double.ToString() formatting (default "G15" truncates a double). Dropping the string-token route plus emitting via the JSON writer's number token with round-trip-safe formatting fixes both the shape and the precision concern in one change:

// System.Text.Json (uses G17 round-trip by default on double)
writer.WriteNumberValue(doubleValue);
// Newtonsoft.Json equivalent
writer.WriteRawValue(doubleValue.ToString("G17", CultureInfo.InvariantCulture));

Edge cases

  • NaN / ±Infinity. These are not expressible in standard JSON. Emit the literal string "UNAVAILABLE" (which the XSD union explicitly permits via UnavailableValueType) rather than a non-standard JSON token like NaN or Infinity.
  • Integer-typed SAMPLEs. Same treatment — JSON number token, not a quoted string. The XSD uses xs:integer-based types for integer samples; the union-with-UnavailableValueType pattern still applies.
  • DATA_SET / TABLE representation SAMPLE values. These serialise as JSON objects, not primitive tokens; the string-quoting bug should not apply to this path but please verify it does not regress when the numeric-token fix lands.
  • EVENT observations with string values. These legitimately stay as JSON strings — the fix here is scoped to numeric SAMPLEs, not all observation values.

Impact

  • Critical. Every numeric Sample value is rejected by strict cppagent-shape consumers (Rust serde::Deserialize with f64-typed fields, C++ nlohmann::json::get<double>, etc.) — every message, not a subtle edge case.
  • Consumers that work around the bug with bespoke "string-or-number" deserialisers pay an ongoing CPU + code-maintenance cost and lose the "UNAVAILABLE" discriminator (cannot tell "1586.66" apart from a malformed "UNAVAILABLE" string without further parsing).
  • Downstream time-series databases typed on DOUBLE PRECISION columns either reject the write or coerce the string through an extra ::float8 cast per row.

Location in source

libraries/MTConnect.NET-JSON-cppagent/Formatters/JsonMqttResponseDocumentFormatter.cs — value writer for numeric Samples. Switch to the JSON writer's number-token method with round-trip-safe formatting; remove the .ToString() intermediate.

Suggested fix

  1. Emit numeric Sample values as JSON number tokens with G17 / round-trip-safe formatting (full double precision preserved).
  2. Emit "UNAVAILABLE" for unavailable values per FloatSampleValueType's union — including the NaN/±Infinity edge case.
  3. Keep EVENT string values as JSON strings — they are legitimately typed xs:string / enumeration.
  4. Add a regression test that round-trips a Sample payload through a numeric-typed deserialiser (e.g. System.Text.Json with a double property) and asserts byte-identical values.

Stability across MTConnect versions

FloatSampleValueType has carried the same xs:float + UnavailableValueType union definition since v1.3+. JSON shape is likewise stable across cppagent's JSON v2 lineage. No delta between v2.5, v2.6, and v2.7 touches this type.

Related issues

References

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