Skip to content
Version 6.1.0
Contact Us

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).

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.


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. The PDU suffix stays upper-case.
  • All confirmed-service request containers carry the Request suffix.
  • The all-caps COV token in element names becomes the camelCase Cov, with two exceptions: the BACnetCOV container is renamed BACnetClientCOV, and the COVIncrement element keeps its all-caps COV. (BACnet service-choice strings such as subscribeCov are unchanged.)
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
  • 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).

SimpleAckPDU was 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:

  1. Spelling. v5 transposed the abbreviation to Adpu. v6 emits the correct Apdu.
  2. 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.

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:

  1. The attribute hundrethSeconds (missing the d) is renamed to hundredthSeconds.
  2. 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).


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 … />).


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:

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}}

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).


  • BACnet wire encoding / decoding.
  • The decode path apart from its entry point: BACnetStack_DecodeAsXML and BACnetStack_DecodeAsJSON became BACnetStack_DecodeAs (above). The other API changes are in the upgrade guide.
  • The serviceChoice enumeration strings (still camelCase: subscribeCov, readProperty, …).
  • The datatype= numeric values.
  • The dual representation pattern: machine-readable in value=, human-readable in element text. (The value= attribute is intentionally retained — it is the stable, scriptable handle.)
  • The Date.year BACnet wire encoding (years-since-1900). New, additive: a yearFull attribute carrying the 4-digit calendar year is now emitted alongside year (omitted for the unspecified/wildcard year). year itself is unchanged.
  • Non-Boolean values remain JSON strings.

  1. Update element-name matchers (XPath / JSONPath / regex) using the rename table in §1.
  2. Update Boolean parsing: XML value= is the string "true"/"false"; JSON _value is now a real boolean.
  3. Rename maxAdpuLengthAccepted → maxApduLengthAccepted and switch from hex enum to integer byte count.
  4. Drop the _StreamAccess / _RecordAccess key lookup; use _context.
  5. Rename _hundrethSeconds → _hundredthSeconds.
  6. Add a unit test asserting your code can parse a Real value of "50.0".
  7. If you string-match attribute quoting, relax to ['"].
  8. JSON list containers are now always arrays — remove single-child coercions for ListOf…, NetworkNumbers, PriorityArray; iterate directly.
  9. JSON empty elements are {} (never null); update any == null checks.
  10. If you need the 4-digit year, read the new yearFull attribute (still absent for unspecified dates).
  11. ConfirmedRequestPDU maxSegmentsAccepted is now a decoded label (unspecified, 2…64, greaterThan64) instead of a hex enum (0x0); update any parsing.
  12. RejectMessageToNetwork now also carries rejectionReasonText (human label) beside the numeric rejectionReason.
  13. Test-tool builds add decode elements for WriteGroup, AddListElement and RemoveListElement. They did not exist in version 5 and are covered separately.