Decoded XML and JSON output: migrating from version 5 to version 6
The v6 release contains breaking changes to the shape of decoded XML and JSON output. The on-wire BACnet behaviour of the stack is unchanged. Apart from the entry-point consolidation described below, only the textual representation produced by the decode helpers has changed.
This document describes every change, why it was made, and how to update consuming code.
For examples of the version 6 output itself, message by message, see the Decoded output reference (XML and JSON).
The entry point: BACnetStack_DecodeAs
Section titled “The entry point: BACnetStack_DecodeAs”v5 had two functions, BACnetStack_DecodeAsXML and BACnetStack_DecodeAsJSON. In v6 both are
replaced by a single function that takes the format as its first argument:
/* removed */ BACnetStack_DecodeAsXML (raw, rawLen, out, outMax, networkType);/* removed */ BACnetStack_DecodeAsJSON(raw, rawLen, out, outMax, networkType);
/* replacement — format is the FIRST argument */BACnetStack_DecodeAs(BACnetDecodeType_XML, raw, rawLen, out, outMax, networkType);BACnetStack_DecodeAs(BACnetDecodeType_JSON, raw, rawLen, out, outMax, networkType);The two were the same function apart from the renderer they selected, so keeping both meant every fix had to be made twice. There is no compatibility shim — the old symbols are gone, and a build that calls them will fail to link rather than silently misbehave.
enum BACnetDecodeType { BACnetDecodeType_XML = 0, BACnetDecodeType_JSON = 1 } is declared in
CASBACnetStackDLL.h. Values are append-only; existing ones will not be renumbered.
The rest of this guide describes the output, which is the same whichever format’s renderer you select.
Behaviour notes: the two STACK_OPTION_DECODE_AS_XML /
STACK_OPTION_DECODE_AS_JSON compile options are not symmetric: a build can contain the XML renderer
without JSON, but JSON requires XML (and STACK_OPTION_STRING_LITERALS, which turns XML on), so a
JSON-only build stops with an #error. Asking for a format that was not compiled returns 0, and a too-small output
buffer still returns 0 without writing or truncating anything.
At a glance
Section titled “At a glance”| Area | v5 example | v6 example |
|---|---|---|
| ACK PDU element | <ComplexACKPDU …> |
<ComplexAckPDU …> |
| Service ACK container | <ReadPropertyACK> |
<ReadPropertyAck> |
| DCC request container | <DeviceCommunicationControl> |
<DeviceCommunicationControlRequest> |
| COV service element | <SubscribeCOVRequest> |
<SubscribeCovRequest> |
ObjectIdentifier value= |
(absent) | value="0,1" |
Boolean value= |
value='1' |
value="true" |
maxAdpuLengthAccepted (PDU attr) |
maxAdpuLengthAccepted='0x5' |
maxApduLengthAccepted="1476" |
datatypeLabel companion |
(absent) | datatypeLabel="Real" |
Real value= |
value='50' |
value="50.0" |
Time attribute name |
hundrethSeconds='0' |
hundredthSeconds="0" |
Time text |
7:5:59.0 |
07:05:59.00 |
StreamAccess context |
<StreamAccess StreamAccess='0'> |
<StreamAccess context="0"> |
| XML declaration | (absent) | <?xml version="1.0" encoding="UTF-8"?> |
| Attribute quoting | value='X' |
value="X" |
| Whitespace | one continuous line | indented, multi-line |
1. Renamed elements (case and suffix normalization)
Section titled “1. Renamed elements (case and suffix normalization)”v5 mixed ACK and Ack capitalization and inconsistently suffixed request containers. v6 normalizes:
- All Acknowledgement container element names use title-case
Ack. ThePDUsuffix stays upper-case. - All confirmed-service request containers carry the
Requestsuffix. - The all-caps
COVtoken in element names becomes the camelCaseCov, with two exceptions: theBACnetCOVcontainer is renamedBACnetClientCOV, and theCOVIncrementelement keeps its all-capsCOV. (BACnet service-choice strings such assubscribeCovare unchanged.)
Rename table
Section titled “Rename table”| v5 element | v6 element |
|---|---|
ComplexACKPDU |
ComplexAckPDU |
ReadPropertyACK |
ReadPropertyAck |
ReadPropertyMultipleACK |
ReadPropertyMultipleAck |
AtomicReadFileACK |
AtomicReadFileAck |
AtomicWriteFileACK |
AtomicWriteFileAck |
GetAlarmSummaryACK |
GetAlarmSummaryAck |
GetEnrollmentSummaryACK |
GetEnrollmentSummaryAck |
GetEventInformationACK |
GetEventInformationAck |
DeviceCommunicationControl |
DeviceCommunicationControlRequest |
SubscribeCOVRequest |
SubscribeCovRequest |
SubscribeCOVPropertyRequest |
SubscribeCovPropertyRequest |
SubscribeCOVPropertyMultipleError |
SubscribeCovPropertyMultipleError |
ConfirmedCOVNotificationRequest |
ConfirmedCovNotificationRequest |
ConfirmedCOVNotificationMultipleRequest |
ConfirmedCovNotificationMultipleRequest |
UnconfirmedCOVNotificationRequest |
UnconfirmedCovNotificationRequest |
UnconfirmedCOVNotificationMultipleRequest |
UnconfirmedCovNotificationMultipleRequest |
COVSubscription |
CovSubscription |
COVMultipleSubscription |
CovMultipleSubscription |
BACnetCOV |
BACnetClientCOV |
OctString (octet-string primitive) |
OctetString |
MaxAPDULengthAccepted (I-Am element) |
MaxApduLengthAccepted |
Integer (signed-integer primitive) |
SignedInteger |
What to change
Section titled “What to change”- Update any XPath, JSONPath, regex, or hand-rolled parser that matches the v5 names above.
- The corresponding JSON keys change with the XML element names (e.g.
ReadPropertyACK→ReadPropertyAck).
SimpleAckPDUwas already title-case in v5 and is unchanged.
2. ObjectIdentifier now carries a value= attribute
Section titled “2. ObjectIdentifier now carries a value= attribute”In v5, every primitive scalar element carried its machine-readable form in a value= attribute except ObjectIdentifier, which only exposed objectType= and objectInstance=. That made generic @value extraction fail for object identifiers.
v6 adds a value= attribute formatted as "<objectType>,<objectInstance>". The existing objectType= and objectInstance= attributes are preserved.
v5
<ObjectIdentifier datatype='12' objectInstance='1' objectType='0'>analogInput, 1</ObjectIdentifier>v6
<ObjectIdentifier datatype="12" datatypeLabel="BACnetObjectIdentifier" objectInstance="1" objectType="0" value="0,1">analogInput, 1</ObjectIdentifier>If your code already parses _objectType + _objectInstance you do not have to change anything. Code that wanted a uniform _value extraction now works for ObjectIdentifier too.
3. Boolean value= attribute uses "true"/"false"
Section titled “3. Boolean value= attribute uses "true"/"false"”In v5, the attribute was numeric (value='1'/value='0') while the element text was true/false. v6 makes the attribute match the element text.
v5
<EndOfFile datatype='1' value='1'>true</EndOfFile>v6
<EndOfFile datatype="1" datatypeLabel="Boolean" value="true">true</EndOfFile>JSON consumers that interpreted _value as numeric (e.g. parseInt) must switch to a string compare (v === "true").
4. maxAdpuLengthAccepted → maxApduLengthAccepted (and the value changes)
Section titled “4. maxAdpuLengthAccepted → maxApduLengthAccepted (and the value changes)”Two changes to the ConfirmedRequestPDU attribute:
- Spelling. v5 transposed the abbreviation to
Adpu. v6 emits the correctApdu. - Value. v5 emitted the raw BACnet enumeration code in hexadecimal (
'0x5'). v6 emits the decoded APDU byte count ('1476').
| v5 raw code | v6 decoded byte count |
|---|---|
0x0 |
50 |
0x1 |
128 |
0x2 |
206 |
0x3 |
480 |
0x4 |
1024 |
0x5 |
1476 |
v5
<ConfirmedRequestPDU invokeId='42' maxAdpuLengthAccepted='0x5' …>v6
<ConfirmedRequestPDU invokeId="42" maxApduLengthAccepted="1476" …>Consumers expecting _maxAdpuLengthAccepted as a hex string must switch to parsing _maxApduLengthAccepted as an integer.
4.1 maxSegmentsAccepted is a label
Section titled “4.1 maxSegmentsAccepted is a label”On ConfirmedRequestPDU, maxSegmentsAccepted used to be the raw enum code in hex (for example 0x0). v6 renders the decoded
value instead: unspecified, 2, 4, 8, 16, 32, 64 or greaterThan64. A value the stack cannot map renders as
unspecified. Update any parser that expects the hex code.
4.2 rejectionReasonText on RejectMessageToNetwork
Section titled “4.2 rejectionReasonText on RejectMessageToNetwork”RejectMessageToNetwork keeps the numeric rejectionReason attribute and now also carries rejectionReasonText, a human-readable
label for the same reason. Nothing needs to change unless you want the text.
5. datatypeLabel companion attribute on every primitive
Section titled “5. datatypeLabel companion attribute on every primitive”v5 emitted only the numeric BACnet Application Tag (datatype='4') with no human-readable label. v6 adds datatypeLabel="…" alongside it.
v5
<Real datatype='4' value='21.5'>21.5</Real>v6
<Real datatype="4" datatypeLabel="Real" value="21.5">21.5</Real>Label mapping:
datatype |
datatypeLabel |
|---|---|
0 |
Null |
1 |
Boolean |
2 |
UnsignedInteger |
3 |
SignedInteger |
4 |
Real |
5 |
Double |
6 |
OctetString |
7 |
CharacterString |
8 |
BitString |
9 |
Enumerated |
10 |
Date |
11 |
Time |
12 |
BACnetObjectIdentifier |
Existing consumers that only read datatype= are unaffected; the new attribute is additive.
6. Real / Double always include a decimal point
Section titled “6. Real / Double always include a decimal point”v5 dropped trailing zeros, so a float of 50.0 was rendered as 50 — visually indistinguishable from an integer. v6 always emits at least one fractional digit.
v5
<Real datatype='4' value='50'>50</Real>v6
<Real datatype="4" datatypeLabel="Real" value="50.0">50.0</Real>If your code did parseFloat, no change is required. If your code did exact string comparison against '50', change it to '50.0'.
7. Time — attribute renamed, text zero-padded
Section titled “7. Time — attribute renamed, text zero-padded”Two changes to <Time> rendering:
- The attribute
hundrethSeconds(missing thed) is renamed tohundredthSeconds. - The element text is now zero-padded to
HH:MM:SS.ss.
v5
<Time datatype='11' hour='7' hundrethSeconds='0' minute='5' second='59'>7:5:59.0</Time>v6
<Time datatype="11" datatypeLabel="Time" hour="7" hundredthSeconds="0" minute="5" second="59">07:05:59.00</Time>JSON consumers must update _hundrethSeconds → _hundredthSeconds. Any code that parsed the text via split(":") and expected single-digit fields must accept zero-padded fields (or use the per-field attributes).
8. Date year is intentionally unchanged
Section titled “8. Date year is intentionally unchanged”year= is a BACnet wire-encoded value (years since 1900: year='126' for 2026) rather than a 4-digit calendar year. In v6 this is preserved on purpose. The XML/JSON attribute mirrors the BACnet spec wire encoding. The human-readable 4-digit year remains in the element text.
v6 (year unchanged from v5; yearFull is new — see §12)
<Date datatype="10" datatypeLabel="Date" day="27" month="5" weekday="3" year="126" yearFull="2026">May, 27, 2026, Wednesday</Date>When consuming _year programmatically, add 1900 to get the calendar year, or read yearFull (absent when the year is unspecified).
9. StreamAccess / RecordAccess context tag attribute
Section titled “9. StreamAccess / RecordAccess context tag attribute”In v5, the context-tag number for <StreamAccess> and <RecordAccess> was emitted as a same-named attribute (StreamAccess='0'), creating the odd <StreamAccess StreamAccess='0'> form. v6 emits the context number under the standard context= attribute used everywhere else.
v5
<StreamAccess StreamAccess='0'> <FileStartPosition datatype='3' value='0'>0</FileStartPosition> …</StreamAccess>v6
<StreamAccess context="0"> <FileStartPosition datatype="3" datatypeLabel="SignedInteger" value="0">0</FileStartPosition> …</StreamAccess>JSON: _StreamAccess / _RecordAccess keys disappear; use _context instead.
10. XML structural changes (declaration, quoting, whitespace)
Section titled “10. XML structural changes (declaration, quoting, whitespace)”v6 XML output is more conventional:
- A standard XML declaration is emitted before the version comment:
<?xml version="1.0" encoding="UTF-8"?>. - Attribute delimiters changed from single quotes to double quotes (
name="value"). - Output is indented with tabs and newlines (previously a single line).
These are presentation changes only; they do not affect well-formedness for any compliant XML parser. String-based regex consumers that hard-coded ' may need to update to " (or relax to ['"]).
v6 example (the Error-PDU frame 810a000d010050c70c9102912a0000000000: BVLL, NPDU and the PDU are siblings under the BACnetPacket root)
<?xml version="1.0" encoding="UTF-8"?><!-- CAS BACnet Stack v6.0.23.0 --><BACnetPacket networkType="IP"> <BVLL function="originalUnicastNPDU" /> <NPDU control="0x00" version="1" /> <ErrorPDU errorChoice="readProperty" originalInvokeId="199"> <Error> <ErrorClass datatype="9" datatypeLabel="Enumerated" value="2">property</ErrorClass> <ErrorCode datatype="9" datatypeLabel="Enumerated" value="42">invalidArrayIndex</ErrorCode> </Error> </ErrorPDU></BACnetPacket>The version comment carries the build’s own version numbers. An element with neither text nor children is written self-closing (<BVLL … />).
11. JSON output
Section titled “11. JSON output”JSON is now produced by an in-house renderer that walks the same decoded object tree as the XML renderer (it no longer round-trips through the third-party xml2json library). The key/attribute/value naming from the sections above all applies to JSON, with attributes prefixed by _ and text content under _text. In addition, v6 makes three JSON-shape changes:
11.1 List containers always emit arrays
Section titled “11.1 List containers always emit arrays”In v5, a list with exactly one child was emitted as a singleton object, but a list with two or more children was an array — so consumers had to coerce defensively. In v6, every BACnet list container (ListOf… elements, plus NetworkNumbers and PriorityArray) always emits its children as a JSON array, even when there is exactly one.
v5
"ListOfReadAccessResults": { "_count": "1", "ReadAccessResult": { "ObjectIdentifier": … } }v6
"ListOfReadAccessResults": { "_count": "1", "ReadAccessResult": [ { "ObjectIdentifier": … } ] }Consumers can now iterate unconditionally and should remove the old Array.isArray(x) ? x : [x] coercions for these containers.
11.2 Boolean _value is a real JSON boolean
Section titled “11.2 Boolean _value is a real JSON boolean”The _value of a Boolean element is now an unquoted JSON true / false rather than the string "true" / "false". (_text remains the string "true"/"false", the human-readable form.) Note this is JSON-only — the XML value= attribute is still the string "true"/"false" (see §3).
v5 "Boolean": { "_text": "true", "_value": "1" }
v6 {"Boolean":{"_text":"true","_datatype":"1","_datatypeLabel":"Boolean","_value":true}}
11.3 Empty elements are {}, not null
Section titled “11.3 Empty elements are {}, not null”An element with no text, attributes, or children (e.g. a WhoIsRequest with no limits) now renders as an empty object {} instead of null. This makes the type stable — a decoded element is always a JSON object (or a bare string for text-only leaves), never null.
v5 "WhoIsRequest": null → v6 "WhoIsRequest": {}
All other JSON values remain strings (numbers, enum codes, hex fields, and composite values like ObjectIdentifier
"56,1"are intentionally not converted to JSON numbers — only Boolean is typed).
12. Things that did not change
Section titled “12. Things that did not change”- BACnet wire encoding / decoding.
- The decode path apart from its entry point:
BACnetStack_DecodeAsXMLandBACnetStack_DecodeAsJSONbecameBACnetStack_DecodeAs(above). The other API changes are in the upgrade guide. - The
serviceChoiceenumeration strings (still camelCase:subscribeCov,readProperty, …). - The
datatype=numeric values. - The dual representation pattern: machine-readable in
value=, human-readable in element text. (Thevalue=attribute is intentionally retained — it is the stable, scriptable handle.) - The
Date.yearBACnet wire encoding (years-since-1900). New, additive: ayearFullattribute carrying the 4-digit calendar year is now emitted alongsideyear(omitted for the unspecified/wildcard year).yearitself is unchanged. - Non-Boolean values remain JSON strings.
Migration checklist
Section titled “Migration checklist”- Update element-name matchers (XPath / JSONPath / regex) using the rename table in §1.
- Update Boolean parsing: XML
value=is the string"true"/"false"; JSON_valueis now a real boolean. - Rename
maxAdpuLengthAccepted→maxApduLengthAcceptedand switch from hex enum to integer byte count. - Drop the
_StreamAccess/_RecordAccesskey lookup; use_context. - Rename
_hundrethSeconds→_hundredthSeconds. - Add a unit test asserting your code can parse a
Realvalue of"50.0". - If you string-match attribute quoting, relax to
['"]. - JSON list containers are now always arrays — remove single-child coercions for
ListOf…,NetworkNumbers,PriorityArray; iterate directly. - JSON empty elements are
{}(nevernull); update any== nullchecks. - If you need the 4-digit year, read the new
yearFullattribute (still absent for unspecified dates). ConfirmedRequestPDUmaxSegmentsAcceptedis now a decoded label (unspecified,2…64,greaterThan64) instead of a hex enum (0x0); update any parsing.RejectMessageToNetworknow also carriesrejectionReasonText(human label) beside the numericrejectionReason.- Test-tool builds add decode elements for WriteGroup, AddListElement and RemoveListElement. They did not exist in version 5 and are covered separately.