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 FAHRENHEIT → CELSIUS (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.cpp — TEST_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
- Emit numeric Sample values as JSON number tokens with
G17 / round-trip-safe formatting (full double precision preserved).
- Emit
"UNAVAILABLE" for unavailable values per FloatSampleValueType's union — including the NaN/±Infinity edge case.
- Keep EVENT string values as JSON strings — they are legitimately typed
xs:string / enumeration.
- 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
Summary
Under the
JSON-cppagent-mqttdocument 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 typeFloatSampleValueType, which is a union ofxs:floatand 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.0with a Python SHDR source feeding<ts>|temp|<float>into a single TEMPERATURE Sample data item. MT.NET emitted:Side-by-side, cppagent v2.7.0.7 in an identical configuration (same device, same SHDR feed, same broker) emitted:
Both agents correctly converted
FAHRENHEIT→CELSIUS(the XML declaresunits="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-Agentsatv6.9.0.2(2025-10-16, latest).eclipse-mosquitto:2.0.22.JSON-cppagent-mqtt.Reproduction
applications/MTConnect-Agent/appsettings.yaml:devices.xml(one TEMPERATURE SAMPLE data item is enough):Feed one numeric value via SHDR:
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 cppagentjson_printer_stream_test.cpp—TEST_F(JsonPrinterStreamTest, samples_and_events_version_2)at the assertionsstream.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 atASSERT_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.xsdand_2.5.xsd):FloatSampleValueTypeis a union ofxs:floatand the"UNAVAILABLE"sentinel. A faithful JSON translation therefore emits:"UNAVAILABLE"when the value is unavailable.Strings in JSON are legitimate only for:
"UNAVAILABLE"sentinel;EVENTobservations whose values arexs:stringor enumeration types (notxs:float);DATA_SET/TABLErepresentation observations (which are JSON objects, not primitive strings).A
VALUE-representation numeric Sample wrapped in quotes matches neither member ofFloatSampleValueType'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-MQTTformat-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 defaultdouble.ToString()formatting (default"G15"truncates adouble). 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:Edge cases
NaN/±Infinity. These are not expressible in standard JSON. Emit the literal string"UNAVAILABLE"(which the XSD union explicitly permits viaUnavailableValueType) rather than a non-standard JSON token likeNaNorInfinity.xs:integer-based types for integer samples; the union-with-UnavailableValueTypepattern still applies.DATA_SET/TABLErepresentation 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.Impact
serde::Deserializewithf64-typed fields, C++nlohmann::json::get<double>, etc.) — every message, not a subtle edge case."UNAVAILABLE"discriminator (cannot tell"1586.66"apart from a malformed"UNAVAILABLE"string without further parsing).DOUBLE PRECISIONcolumns either reject the write or coerce the string through an extra::float8cast 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
G17/ round-trip-safe formatting (fulldoubleprecision preserved)."UNAVAILABLE"for unavailable values perFloatSampleValueType's union — including theNaN/±Infinityedge case.xs:string/ enumeration.System.Text.Jsonwith adoubleproperty) and asserts byte-identical values.Stability across MTConnect versions
FloatSampleValueTypehas carried the samexs:float+UnavailableValueTypeunion 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
Header.versionreports library assembly version instead of MTConnect release #127 —Header.versionreports library assembly version — adjacent Header-serialiser defect on every envelope.FloatSampleValueTypeis unchanged across the uplift, but a coverage test fixture added here should validate against v2.6 / v2.7 schemas once the library ships them.References
FloatSampleValueType+UnavailableValueType):MTConnectStreams_2.7.xsdMTConnectStreams_2.6.xsdMTConnectStreams_2.5.xsdsrc/mtconnect/printer/json_printer.hpp+json_printer.cpp.test_package/json_printer_stream_test.cpptest_package/json_printer_probe_test.cpptest_package/json_printer_asset_test.cpptest_package/json_printer_error_test.cppmtconnect/cppagent#275(closed; confirms v1 uses arrays-of-wrappers, v2 uses object-keyed-arrays, both emit numeric values as JSON number tokens).mtconnect/cppagent_dev#275— never resolved.