Skip to content

Data Types

Kevin Herron edited this page Oct 5, 2026 · 11 revisions

Milo 1.2.0-SNAPSHOT, Java 17. API baseline 7faaf4f0516c.

OPC UA values carry a declared type, shape, and sometimes a quality/timestamp envelope. Choose Java values that match all three; a numerically similar Java type is not automatically interchangeable on the wire. Use Getting started for dependencies and OPC UA concepts for NodeId/namespace identity.

Built-in mappings and unsigned values

OPC UA type Typical Milo/Java representation
Boolean Boolean
SByte, Int16, Int32, Int64 Byte, Short, Integer, Long
Byte, UInt16, UInt32, UInt64 UByte, UShort, UInteger, ULong
Float, Double Float, Double
String, Guid String, UUID
DateTime DateTime
ByteString, XmlElement ByteString, XmlElement
NodeId, ExpandedNodeId, QualifiedName, LocalizedText The correspondingly named Milo types
StatusCode, DataValue, Variant The correspondingly named Milo wrappers
A structured value in a Variant Usually an ExtensionObject, decoded using a matching context
Enumeration on the generic wire path An Int32 value; typed codecs can expose enum classes. JSON VERBOSE uses named enum strings in structure fields.

The factories in org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned distinguish numeric conversion from bit reinterpretation. ubyte(byte), ushort(short), uint(int), and ulong(long) preserve the supplied bits, so uint(-1) is 4,294,967,295. The wider numeric/text overloads, such as uint(long) and ulong(BigInteger), reject out-of-range magnitudes with NumberFormatException. Inspect UInt32 with longValue() and UInt64 with toBigInteger(); ULong.longValue() returns the raw signed bits.

Variant infers its wire type from the Java value: new Variant(42) is Int32 and new Variant(42L) is Int64. A Java byte[] is an SByte array; wrap bytes with ByteString.of(bytes) for a ByteString. The constructor does not validate its argument. Prefer Variant.of(...) to reject unsupported classes, scalar nested Variants, DiagnosticInfo, and multidimensional Java arrays, or use typed factories such as ofUInt32, ofStruct, and ofMatrix. Variant arrays are allowed; a Variant directly containing another Variant is not.

Arrays, Matrix, and DataValue

A one-dimensional array and a scalar have different ValueRanks. A Matrix carries flattened elements, dimensions, and element-type metadata for multidimensional values. Prefer a nested-array factory such as Matrix.ofUInt32(UInteger[][]) when possible. With a flat constructor, the application must ensure that the element count equals the dimension product. Constructors use Java assertions for basic shape/type checks and do not check that product. Binary Variant encoding can emit a count mismatch that fails only when decoded with Bad_DecodingError.

Binary decoding produces boxed arrays. JSON decoding produces primitive arrays for Boolean, SByte, Int16, Int32, Int64, Float, and Double, both in Variant arrays and flattened Matrix elements; unsigned and object types remain object arrays. For example, the same Int32 matrix has Integer[] elements after Binary decoding and int[] after JSON decoding. Inspect Matrix.getElementType() or access elements with java.lang.reflect.Array instead of assuming one array class. This difference also exists in 1.1.7.

Matrix.ofNull() represents a null Matrix; test it with isNull(). Binary Matrix field decoders return that object in 1.2, where 1.1 returned Java null. Inside a Binary Variant, Matrix.ofNull() instead becomes a null Variant whose Java payload is null. Empty Matrix dimensions can change when encoded inside a Variant; see encoding boundaries.

This fragment uses uint(...) from Unsigned and Instant from java.time:

Matrix grid =
    new Matrix(
        new UInteger[] {uint(1), uint(2), uint(3), uint(4_000_000_000L)},
        new int[] {2, 2},
        OpcUaDataType.UInt32);
DataValue sample =
    new DataValue(
        new Variant(grid),
        StatusCode.GOOD,
        new DateTime(Instant.parse("2026-01-01T00:00:00Z")));

The matrix has two rows and two columns; its last UInt32 is 4,000,000,000. Encoding and decoding it through the current JSON mapping preserves that magnitude, dimensions, and both timestamps. This three-argument DataValue constructor sets both source and server time to the supplied value. Use DataValue.valueOnly(...) for no timestamps, or DataValue.newValue() to set them independently.

Inspect DataValue.statusCode() before consuming its payload. A successful service response can contain a DataValue with bad quality. Java null, null Matrix, empty array, null/empty ByteString, an absent optional member, and a bad DataValue with no usable value are distinct states.

Discover and decode a custom structure

A client can use a registered Java codec or discover definitions from the server. The usual read workflow is:

  1. Connect and resolve the Variable NodeId using the server's namespace URI.
  2. Read the DataValue and check its status.
  3. Decode its ExtensionObject with the connected client's matching encoding context.
  4. Inspect the returned structure type and fields before using them.

The type-tree and dynamic-manager factories both default to eager discovery. The first getDynamicEncodingContext() builds the tree and codecs with network I/O. Configure factories before discovery; lazy factories resolve custom metadata on demand. Discovery must succeed under the Session's access and operation limits. A lazy resolution failure is cached until clearFailedResolutions() on the lazy manager or the next Session reset.

Every Session establishment resets the client's tree, dynamic manager, and dynamic context. Fetch the dynamic context again after reconnecting; retained decoded values still refer to their previous metadata. Use getStaticEncodingContext() with registered codecs when you need the corresponding generated or application Java classes.

With a connected OpcUaClient and a Variable NodeId resolved through the server's namespace URI, this helper checks quality and decodes an ExtensionObject:

static UaStructuredType readStructure(OpcUaClient client, NodeId variableId) throws UaException {
  DataValue value = client.readValue(0, TimestampsToReturn.Both, variableId);
  if (!value.statusCode().isGood()) {
    throw new UaException(value.statusCode());
  }
  if (!(value.value().value() instanceof ExtensionObject encoded)) {
    throw new IllegalArgumentException("Expected a structured value");
  }
  return encoded.decode(client.getDynamicEncodingContext());
}

For the integration fixture's TestNamespace custom matrix structure, the result is DynamicStructType; its getMembers() map includes BuiltinMatrix, EnumMatrix, and StructMatrix. The built-in matrix contains Int32 values 0 through 3 with dimensions 2 by 2. The fixture verifies the discovered type identity as well as payload. A missing Variable raises Bad_NodeIdUnknown before decoding. A scalar Variable raises the helper's IllegalArgumentException. Decode failures propagate as unchecked UaSerializationException. Check the actual structure type rather than blindly casting every result to DynamicStructType.

The namespace table, definitions, encoding IDs, and codecs must describe the same model generation. ExtensionObject.decode(...) caches its first successful result per object, regardless of later context or encoding arguments. A second call cannot switch a dynamically decoded value to an application class. Read a fresh ExtensionObject after changing registrations or contexts, and handle retained values explicitly across model changes. An unknown codec typically raises Bad_DecodingError; an absent JSON/XML encoding module raises Bad_EncodingError with "encoding not registered".

Optional fields, unions, and subtypes

DynamicStructType.getMembers() is keyed by declared field names. For a StructureWithOptionalFields, the presence of a map key selects the optional field. Omitting the key omits that field; including the key with a null value selects the field and passes null to its encoder. That works only for field types that accept null. Do not infer absence solely from map.get(...) == null. Names are not validated: unknown keys are ignored, an optional-field typo omits the field, and a required-field typo supplies null. Build keys from getDataTypeDefinition().getFields() and keep required fields and declared scalar/array ranks consistent.

DynamicUnionType carries at most one UnionValue(fieldName, fieldValue). No selected member is represented by a null UnionValue; a selected nullable member with a null payload is a different state. getValue() and is(fieldName) expose the selection. Unknown names fail encoding; an invalid wire switch value fails decoding.

Subtype-enabled fields must identify the actual structured type. Dynamic codecs can return UaStructuredType[] for subtype-enabled structure arrays and ExtensionObject arrays inside Variant fields; do not cast those arrays to one concrete dynamic type. Null ExtensionObjects in those arrays become Java null. If decoding an ExtensionObject in a BaseDataType/Variant field fails, the dynamic codec retains the raw ExtensionObject value or array and logs at debug level. Preserve array shape separately from which subtype each element uses.

Register a codec

Use a GenericDataTypeCodec<T> when the application has a stable Java representation. Implement getType(), encodeType(...), and decodeType(...), with consistent field order/names and matching scalar, array, enum, Matrix, and structure calls. The value's UaStructuredType identity methods must match server metadata. Register both the DataType ID and available encoding IDs, resolved against the actual namespace table:

client
    .getStaticDataTypeManager()
    .registerType(dataTypeId, codec, binaryEncodingId, xmlEncodingId, jsonEncodingId);

Here codec is the application codec. registerType overwrites existing type and supplied encoding lookups, including built-in registrations. A null or OPC UA null encoding ID leaves that existing mapping unchanged. For a temporary registration, acquireType(...) rejects collisions with IllegalStateException and returns a RegistrationHandle; keep it while the codec is needed and close it afterward. A custom manager that does not support this method throws UnsupportedOperationException. The local fixture registers its MatrixTestType.Codec and then decodes the same server value into that Java class through client.getStaticEncodingContext(). Registration is local; it does not publish a DataType node to the server or make an incompatible peer understand the model.

On a server, publish the DataType, definition, type hierarchy, and encoding relationships, and register the corresponding codec with the server's static manager. See ExampleNamespace.registerCustomStructType for a server registration. For XML, XmlDataTypeCodec supplies declaring-model mappings for inherited fields. The encoding context's getXmlNamespaceUris() maps those model URIs to XML schema namespaces; use withXmlNamespaceUris(...) to add authoritative mappings. Without one, the model URI is used as its XML namespace. For JSON optional/union headers, use the dedicated encoding-mask/switch APIs described in Encodings.

Legacy dictionaries

The legacy dictionary reader, manager, and corresponding dtd-core packages are marked deprecated for removal at package level since 1.0.0 (the GDS dictionary package since 1.2.0); that annotation does not produce normal compiler warnings. Use the reader for peers that expose only DataTypeDictionaries, and prefer DataTypeDefinition-based designs for new models.

Servers that expose only the older DataTypeDictionary mechanism need the legacy initializer in milo-dtd-reader. Configure DataTypeManagerFactory.eager(new LegacyDataTypeManagerInitializer(client)) through setDynamicDataTypeManagerFactory(...) before discovery, as shown in the linked legacy example. This is an explicit alternative to the default DataTypeDefinition mechanism, not a guarantee of automatic fallback or support for every vendor dictionary.

That example targets an external Unified Automation demo server and a vendor-specific Variable; its hardcoded address and namespace indexes are not local tutorial defaults. Supply your own reachable server, trust/identity, model, and resolved NodeIds, then validate dictionary parsing and reads/writes there. The original example body was exercised against a local Milo BSD dictionary fixture. This does not establish interoperability with the external vendor server.

Ownership

Keep codec registries scoped to the client/server whose tables they use. DefaultEncodingContext starts with only namespace 0 and returns the JVM-wide OpcUaDataTypeManager.getInstance(). Do not register one application's model in that shared singleton. Keep I/O out of encode/decode callbacks; discover metadata beforehand or let the configured client resolver own it. Disconnect owned clients and dispose application buffers/writers through their own lifetime boundaries. If encoding fails before a Write is sent, that call performs no server write. Successful encoding says nothing about whether the server accepts the value.

Examples and reference

Related pages

Reference · Encodings · Client nodes · Server data access

Clone this wiki locally