Repository navigation
Encodings
Milo 1.2.0-RC1, Java 17. API baseline 69cb25087804.
This page is the reference for serializing OPC UA values with Milo's Binary, XML, and JSON encoders. You need it when you store or exchange values outside an OPC TCP connection, write a custom codec, or migrate JSON that Milo 1.1 wrote.
Every encode and decode runs against a context that supplies the matching namespace tables, limits, and codecs. An encoding module only serializes values. It does not select a network transport or supply an HTTP, WebSocket, or PubSub runtime.
When you store encoded data, choose a context with the matching model and namespace tables, and select the encoding explicitly. Keep the format version and the namespace identities with the encoded value, because reading it back depends on both. Later, decode it with the matching context and check the result before you rely on the stored representation.
The modules you add decide which formats are available, and the context you pass decides which types and namespaces an encoder understands.
| Format | Module | Notes |
|---|---|---|
| Binary | milo-stack-core |
Serves the normal OPC TCP transport |
| XML | milo-encoding-xml |
In 1.2 it no longer brings in JAXB dependencies, so applications that use JAXB must declare it themselves |
| JSON | milo-encoding-json |
The separate codec-json project is an incubating experiment, not this module |
Use the same Milo version for all three. See Getting started for version alignment.
An EncodingContext supplies the DataTypeManager, EncodingManager, namespace and server tables, XML namespace mappings, and EncodingLimits. DefaultEncodingContext starts with namespace 0 and uses the shared JVM-wide OpcUaDataTypeManager singleton. Use it for standard-type examples like the ones on this page, but do not install application codecs into that singleton. For application-defined types, use a client or server context that has the matching model metadata. A namespace index is meaningful only against that context's table.
UaEncoder and UaDecoder define the primitive, array, Matrix, enum, and structure operations that all three formats share. The transport-https and transport-websocket modules are incubating and unpublished, and WebSocket subprotocol negotiation does not provide a JSON message codec.
Use this section when you need a structure in a particular format, for example to store it or hand it to another system. An ExtensionObject records an encoded structure together with its encoding or type identity, so it is the unit you encode and decode.
In this fragment, context is new DefaultEncodingContext() and encoding is one of OpcUaDefaultBinaryEncoding.getInstance(), OpcUaDefaultXmlEncoding.getInstance(), or OpcUaDefaultJsonEncoding.getInstance():
Range original = new Range(0.0, 100.0);
ExtensionObject encoded = ExtensionObject.encode(context, original, encoding);
Range decoded = (Range) encoded.decode(context, encoding);All three encodings reconstruct the Range with Low 0.0 and High 100.0. Binary and XML ExtensionObjects identify their encoding node, while a JSON ExtensionObject uses the DataType NodeId. The overload of ExtensionObject.encode(...) without an encoding argument uses Binary. The selected encoding module and the codec must both exist. If a required JSON or XML module is absent, decoding fails with Bad_EncodingError and "encoding not registered".
An ExtensionObject caches its first successful decode, and later calls return that result even when they pass a different context or encoding. After changing contexts or registrations, use a fresh ExtensionObject, such as a new read result. transcode(...) catches a serialization failure, logs it, and returns the original object, so check its result instead of assuming the conversion succeeded.
The ExtensionObject path above hides intermediate buffers. When you use an encoder or decoder directly, you own the resources it works on:
| Encoder or decoder | What to close or release |
|---|---|
| Binary encoder or decoder | Release the Netty ByteBuf
|
| JSON encoder | Close the encoder. Closing it also closes the supplied Writer. |
| XML encoder | Close the encoder, which closes its XMLStreamWriter. The application still owns the underlying Writer. |
Also close any other streams or writers you own. Do not share mutable encoder or decoder instances across concurrent operations. Even on the high-level ExtensionObject path, the application still owns external input and output resources.
Read this when you produce or consume Milo JSON, especially when another product reads it. The 1.2 mapping follows the OPC UA 1.05 representation, with differences that include those noted below. Valid JSON syntax alone does not make a valid OPC UA value, because type identity and field rules still apply.
The encoder and decoder both default to COMPACT, and the decoder does not detect the mode. For VERBOSE input, call decoder.setEncoding(OpcUaJsonEncoder.Encoding.VERBOSE) before decoding. Match the decoder to the encoder for structures with optional fields, unions, or enums, because the two modes write them differently. For example, COMPACT writes enum fields as numbers, and VERBOSE writes named enums as Name_Value strings. A mismatch in either direction can raise Bad_DecodingError for unexpected members or incompatible enum tokens.
With a standard EncodingContext, this fragment writes and reads a scalar Int32 DataValue. It belongs inside a method that declares or catches Exception, because the encoder's close() declares it:
String json;
try (var encoder = new OpcUaJsonEncoder(context)) {
encoder.encodeDataValue(null, DataValue.valueOnly(new Variant(42)));
json = encoder.getOutputString();
}
DataValue decoded = new OpcUaJsonDecoder(context, json).decodeDataValue(null);The fragment produces this exact output, which its decoder then parses:
{"UaType":6,"Value":42}The type tag and Value share the DataValue object, and status and timestamp fields are siblings when present. A Matrix keeps its dimensions alongside its flattened values. JSON decoders return primitive arrays for several built-in types, as described in array representations.
A Java null element in a Variant array has no single JSON rule. It depends on the element type:
| Element type | Encoding a Java null element | Decoding the result |
|---|---|---|
| String | JSON null, position kept | Java null |
| ByteString | JSON null, position kept | ByteString.NULL_VALUE |
| Boolean, SByte, Int16, Int32 | JSON null | Fails with Bad_DecodingError
|
| Byte, UInt16, UInt32, Int64, UInt64, Float, Double | The encoder throws an unchecked NullPointerException instead of UaSerializationException
|
Nothing to decode |
| DateTime | 0001-01-01T00:00:00Z |
A DateTime object whose isNull() is false |
| Other element types | No common rule | No common rule |
The Boolean and numeric rows apply in both JSON modes, and the DateTime row describes a Milo-to-Milo Variant array round trip. DateTime.NULL_VALUE behaves differently from a Java null. It is written as 1601-01-01T00:00:00Z and decodes back to DateTime.NULL_VALUE. This differs from OPC 10000-6 1.05.07 §5.4.2.6, which defines 0001-01-01T00:00:00Z as the null DateTime. Map DateTime nulls explicitly when exchanging JSON with other products, and test that mapping with each peer.
Reject or replace null Boolean and numeric elements before encoding a Variant array, and check other element types against the representation your application needs. A Java null does not imply a JSON-null round trip.
How an ExtensionObject appears in JSON depends on how its body was encoded. Only a structure that was encoded as JSON appears as inline fields:
| ExtensionObject | JSON form |
|---|---|
| Binary, including one read over OPC TCP | An envelope with UaTypeId, UaEncoding 1, and a base64 UaBody
|
| XML | An envelope with UaTypeId, UaEncoding 2, and a base64 UTF-8 UaBody
|
| Explicitly encoded as JSON | Inline fields, with a UaTypeId that identifies the DataType and no UaBody wrapper |
A UaStructuredType placed directly in a Variant |
It first takes the Binary path, so it also stays opaque |
To get inline fields, explicitly encode the structure as JSON first, then encode the DataValue that holds it. Here context is new DefaultEncodingContext(), range is new Range(0.0, 100.0), and the enclosing method declares throws Exception:
ExtensionObject jsonStructure =
ExtensionObject.encode(context, range, OpcUaDefaultJsonEncoding.getInstance());
String inline;
try (var encoder = new OpcUaJsonEncoder(context)) {
encoder.encodeDataValue(null, DataValue.valueOnly(new Variant(jsonStructure)));
inline = encoder.getOutputString();
}The resulting JSON includes a UaTypeId that identifies the DataType and a High field of 100, with no UaBody wrapper. It decodes to the same Range.
To convert a Binary or XML body you received, decode it with its matching context, then explicitly encode the returned structure as JSON. Wrapping the Binary or XML ExtensionObject in JSON directly preserves the opaque body and leaves its fields unconverted.
Wrapping needs a reverse getDataTypeId(NodeId) lookup, and an unresolved DataType ID raises Bad_EncodingError. Custom managers must implement this lookup, because its default returns null. The client's LazyClientDataTypeManager performs the reverse lookup only on registrations already present, and it does not discover an unresolved custom type during getDataTypeId(...). For a freshly read custom Binary ExtensionObject, first resolve manager.getCodec(encoded.getEncodingOrTypeId()) and check it for null, or decode the structure before explicitly encoding it as JSON.
You need this section if you stored JSON that Milo 1.1 wrote, or if you maintain custom codecs written for 1.1. Keep an explicit format version with persisted JSON, because a successful trial decode does not identify the version that wrote it.
| 1.1.7 payload | 1.2 behavior |
|---|---|
A bare scalar Int32 Variant, which already uses UaType and Value
|
Decodes unchanged. Compatibility depends on the payload, not just the outer Variant shape. The 1.2 decoder also accepts the older Type and Body aliases for Variant members. |
A DataValue, which nests that Variant under Value
|
Rejected with Bad_DecodingError
|
A JSON-native ExtensionObject, which wraps fields under UaBody and already uses the DataType ID |
The envelope reads, but decoding the structure then fails because the old wrapper is not unwrapped |
A Binary or XML ExtensionObject envelope, which uses its encoding ID as UaTypeId
|
Rejected with Bad_DecodingError before the inner structure is decoded, because 1.2 expects the DataType ID. A bare Variant containing such an envelope is rejected too. |
An XML ExtensionObject body, which 1.1.7 stored as XML text in UaBody
|
1.2 expects base64 UTF-8, so replacing only the ID is not enough for XML |
Two more differences affect stored data. The 1.1.7 encoder already collapses a null ByteString into an empty string, so a migration preserves the resulting empty bytes but cannot recover the lost null identity. The null Matrix field representation also changed. The same Binary bytes decode to Java null in 1.1.7 and to Matrix.ofNull() in 1.2.
The two Milo versions use the same Java package names, so they cannot share a class loader. Convert in separate processes or isolated class loaders, with the old and new codecs each matched to their model:
- In the old process, read the explicit format version and decode the old JSON. Check quality, value types, and shape before exporting.
- Write an intermediate format that both versions understand. OPC UA Binary plus a versioned manifest works. For application models, record the old process's namespace URI table in index order alongside the intermediate, because Binary NodeIds and encoding IDs carry indexes. Decode JSON and XML ExtensionObject bodies and re-encode them as Binary first instead of relabeling their bytes.
- In the 1.2 process, restore the namespace table from the recorded URIs in the same index order, and read the intermediate with the matching codecs. Check the results, then explicitly encode them in the current JSON representation. Decode that JSON again and compare semantic results before replacing stored data.
Run the old process to completion first, and keep its exchange files until the 1.2 process finishes.
Binary carries the data between the two processes, but it is not a blanket compatibility guarantee. Reject invalid dimensions and count mismatches before export. Test application-specific structures, optional fields, unions, empty values, and actual stored records separately, and keep backups. See the versioned migration guide.
Custom codecs must replace generic UInt32 writes and reads of an EncodingMask or SwitchField with encodeEncodingMask, decodeEncodingMask, encodeSwitchField, and decodeSwitchField. The decoder methods take declared field names, which lets VERBOSE JSON associate named optional and union fields correctly. For that reason, a codec that happens to work in Binary is not necessarily valid in VERBOSE JSON. Use the corresponding struct, enum, and Matrix methods as well, instead of hand-encoding each format's representation.
XML field namespace mappings follow the declaring model, so they can differ for inherited fields. They are separate from namespace indexes in NodeIds. Use XmlDataTypeCodec for declaring-model mappings and EncodingContext.withXmlNamespaceUris(...) to map model URIs to schema namespaces. Without a context mapping, the model URI is used as the XML namespace. Keep namespace information in the input instead of stripping it to make the input appear compatible.
Check these rules when input comes from outside your process, or when you encode shapes and identifiers at the edge of what a format supports. Several cases succeed silently and change the value, so enforce your own invariants before encoding.
JSON decoding limits input to MaxMessageSize UTF-16 code units, including whitespace. A value of zero or less still applies the default limit of 2,097,152 code units. MaxRecursionDepth bounds buffered containers and nested ExtensionObject decoding, but it does not count the streamed outer DataValue or Variant wrapper. Exceeding either bound raises Bad_EncodingLimitsExceeded. These limits are separate from OPC TCP framing and service operation limits, which Configuration covers.
The decoder accepts JSON object members in any valid order, but it checks for duplicate members only in some places:
| Where the duplicate appears | Result |
|---|---|
| Structures, and buffered Variant and ExtensionObject bodies | Rejected |
| Inside a buffered Value object | Fails with Bad_DecodingError
|
Top-level DataValue and Variant members, including Value, Dimensions, Status, timestamps, and the UaType and Type aliases |
Not checked. The last occurrence wins. |
| Directly streamed built-in objects such as LocalizedText | Not checked. The last occurrence wins. |
Because of the unchecked cases, do not use this decoder as a general duplicate-key validator. If your application needs that guarantee, reject duplicate keys before decoding.
Unexpected DataValue members and a nested Variant without the required sibling type tag fail with Bad_DecodingError. A truncated Binary Range and an out-of-range XML Int32 also fail with decoding errors. XML arrays must use the expected direct-child structure, and invalid lexical values, integer overflow, and date-only DateTimes fail with Bad_DecodingError. Treat these failures as diagnostics, not as a reason to accept malformed data as a different type.
Test custom codecs in every format you use, covering namespaces, optional-field and union headers, empty and null values, and unread extra fields.
Each format handles empty and inconsistent Matrix shapes differently:
| Where the Matrix is encoded | Behavior |
|---|---|
| Matrix inside a Binary Variant, empty, with a zero or negative dimension | Becomes an empty one-dimensional array |
| Matrix inside a Binary Variant, nonempty | Fails with Bad_EncodingError unless it has at least two dimensions and every dimension is positive. A positive dimension product that differs from the element count can encode successfully but fails decoding with Bad_DecodingError. |
Binary structure-field encodeMatrix(...)
|
Preserves nonnegative dimensions such as [0, 2]. If any dimension is zero and none is negative, the encoder writes no elements, so a nonempty Matrix silently decodes as empty. With all dimensions positive, only a Java assertion checks the element count, and the JVM disables assertions by default. The encoder can write a negative dimension, but the field decoder rejects it with Bad_DecodingError. |
| XML structure-field Matrix, empty (no elements and a zero or negative dimension) | Written as nil and decodes as Matrix.ofNull(). Its original dimensions and the empty-versus-null distinction are lost. |
Enforce shape and element-count invariants in application code before encoding, and account for these format-specific conversions when handling null and empty values.
Namespace problems in JSON often succeed without an error and change identity instead:
| Case | Result |
|---|---|
| Decoding a NodeId with an unknown namespace URI | No error. The NodeId becomes a namespace-0 String NodeId containing the original text. |
| Decoding a QualifiedName with an unknown namespace URI | No error. It becomes a namespace-0 name containing the original text. |
| Encoding a QualifiedName or an index-based ExpandedNodeId whose namespace index is missing | Bad_EncodingError |
| Encoding a NodeId whose namespace index is missing | The NodeId becomes a namespace-0 String identity containing its original parseable text, so numeric ns=5;i=1 encodes as s=ns=5;i=1
|
In the silent cases, the result does not identify the intended node or browse name. Validate the namespace table before encoding.
The encoder writes a nonzero QualifiedName namespace as nsu=<uri>;<name>. A numeric prefix such as 1:Name decodes as namespace 1, even when that text was the name of a namespace-0 QualifiedName. Such a name does not keep its identity through this string grammar.
An unregistered codec raises Bad_DecodingError on decode. Handle UaSerializationException and inspect its status and cause, including when the selected encoding does not match the ExtensionObject kind. Validate array elements before encoding, because the JSON null-element cases can throw an unchecked NullPointerException. A successful local round trip proves a representation only for the tested context, so test exchange with each peer product separately.
Encoding contracts and implementation:
- Stack module list
- Shared encoder contract
- JSON encoder
- JSON decoder and limits
- XML codec namespace adapter
- ExtensionObject identity and conversions
Encodings decide how a value is written. When the value crosses a network, the SecureChannel's security policy decides how it is protected, so continue with Security policies.
Related: Reference · Data types · Configuration · 1.2.0 migration guide
User guide for Milo 1.2.0-RC1 at source 69cb25087, 2026-10-12. Home · Getting started · Examples · Troubleshooting · Release notes
- Home
- Start here
- Client guide
- Server guide
- Feature guides
- Reference
- Operations
- Examples
- Release notes
- Contributing