OPC UA Array Index Ranges: Validate Partial Reads and Writes Before Mapping Them
OPC UA NumericRange uses zero-based, inclusive indexes and a range for every dimension of a numeric array. A read can return a clipped selection when an upper bound extends past the array. A write must supply the requested array shape and cannot claim success when all requested elements cannot be written. Validate syntax, dimensions, bounds and write capability separately.
This guide pins OPC UA Part 4, Services, version 1.05.07, especially sections 7.27 and 5.11.4. The worked model covers rectangular numeric arrays. String and ByteString substring rules, sparse application structures and SDK-specific Variant layouts are excluded. A local Python fixture was executed; no device write or OPC UA server was used.
Parse the range before touching array storage
A null or empty range means the parameter is unused. For array access, that selects the complete value. It does not select an empty array and should not be silently substituted for an invalid user-entered range.
A single integer selects one index. A colon separates the inclusive lower and upper indexes of a range, and the lower value must be strictly less than the upper. Therefore 2 selects one element, while 2:2 is invalid syntax. Whitespace is not permitted.
Commas separate dimensions. For a two-dimensional array, 1,2 selects the element at row one, column two. A comma is not a list of independent elements in a one-dimensional array. Preserve dimension order as defined by ArrayDimensions.

Separate syntax from array compatibility
| Input | Interpretation for a 3-by-4 numeric array |
|---|---|
| 1:2,1:3 | Rows one through two, columns one through three: six elements |
| 1,2 | One element at row one, column two |
| 1:2 | Dimension count does not match this array |
| 2:2,0 | Invalid colon-range syntax |
| 1:9,2:8 | Valid syntax; read upper bounds exceed current dimensions |
| 3:8,0:1 | Valid syntax; row lower bound is beyond the array |
Part 4 section 7.27 distinguishes invalid syntax from a syntactically valid selection without usable data. For reads, a lower bound outside a dimension yields Bad_IndexRangeNoData; an excessive upper bound yields partial results. Do not map every failed selection to “malformed request.”
Record the operation-specific status too. The Write service’s status table additionally allows Bad_IndexRangeInvalid when range writes are generally supported for a node but that supplied range cannot be written. A generic parser’s error categories must not erase the service’s more specific contract.
Calculate the result shape explicitly
For each inclusive range, the requested extent is upper minus lower plus one. Multiply the extents only after checking every dimension and guarding arithmetic overflow. A small textual range can describe an enormous product, so impose application limits before allocating a result buffer.
In the hypothetical array, element value equals ten times the row plus the column. Range 1:2,1:3 returns 11, 12, 13, 21, 22 and 23 in the model’s explicit row-major traversal.
Range 1:9,2:8 clips on read to rows one and two and columns two and three, producing 12, 13, 22 and 23. A client expecting the requested eight-by-seven rectangle must inspect the actual returned dimensions and length rather than indexing a presumed larger response.
Record current ValueRank and ArrayDimensions, but still validate the returned value. A model’s array size can change. Cached dimensions are qualification evidence, not permission to copy an unchecked response into a fixed-size application object.
Require an array payload for partial writes
Section 5.11.4 requires the supplied Value to be an array when indexRange is specified, even for a single selected element. A scalar 99 and a one-element array containing 99 are different write payloads for this purpose.
The replacement must match the requested selection. For the six-element rectangle above, provide the appropriate array representation and dimensions required by the selected SDK and node datatype. Do not flatten blindly when the API requires a multidimensional Variant.
The local fixture accepts a one-element list for 1,2 and rejects a scalar, an empty list and a two-element list. It also rejects the out-of-bounds write before mutating its local array. That is a model invariant, not a claim that every server offers transaction rollback for arbitrary write failures.
Do not emulate an unsupported partial write silently
If the node cannot support writing indexRange, the service specifies Bad_WriteNotSupported. Reading the whole array, modifying one element and writing everything back changes the concurrency contract. Another writer may update a different element between those two operations.
Use an explicitly supported whole-value ownership policy or a server-defined operation if partial writes are unavailable. Do not make that fallback invisible behind a “partial update succeeded” message. Qualify permissions, datatype acceptance and process semantics independently of syntactically valid indexes.
Multiple Write operations in one service call also do not form a general atomic transaction. Their processing order is not defined, and some can succeed while others fail. Avoid using several overlapping ranges to express a required ordered transformation without an explicit server guarantee.
A safe qualification matrix
- Read node datatype, rank and dimensions, then verify a whole-value response in an isolated test model.
- Test one index, a complete valid rectangle and boundaries at the last element of each dimension.
- Test equality, reversed bounds, whitespace and a missing dimension as separate invalid cases.
- Compare upper-bound-clipped reads with lower-bound-outside reads and inspect actual returned shape.
- On an authorized harmless array, test a correctly shaped partial write and verify unchanged neighboring elements.
- Exercise unsupported range writes and payload-shape rejection without enabling automatic whole-array replacement.
The executed fixture covered the concrete rectangles, eight syntax rejections, rank and lower-bound failures, four single-element payload forms and pre-mutation rejection of an oversized write. Python’s unbounded integers do not test a C decoder’s overflow handling. Product acceptance still requires bounded parsing, SDK representation checks and real per-operation statuses under the selected server version.