Type serialization
TypeSerializer turns OPC UA value objects into JSON and back. Every type used in the query path has a deterministic shape — useful to know when debugging a wire capture or writing a non-PHP client.
PhpOpcua\SessionManager\Serialization\TypeSerializer converts
between PHP Types\* / Module\* value objects and the JSON
representation the IPC query path uses. Every common type has a
fixed shape — this page is the reference.
queryvsinvoke. This page covers thequerypath's JSON shapes. Theinvokepath usesopcua-client's Wire registry instead — typed payloads carry an explicit__tdiscriminator. See Envelope and framing — typed payloads for invoke.
Core value objects
NodeId
Numeric: {"ns": 0, "id": 2261, "type": "numeric"}
String: {"ns": 2, "id": "Devices/PLC/Speed", "type": "string"}
GUID: {"ns": 0, "id": "72962B91-FA75-4AE6-8D28-B404DC7DAE63", "type": "guid"}
Bytes: {"ns": 3, "id": "<base64>", "type": "opaque"}
| Field | Meaning |
|---|---|
ns |
Namespace index (int ≥ 0) |
id |
Identifier value — type-dependent representation |
type |
One of numeric, string, guid, opaque |
The keying matches NodeId::numeric() / ::string() / ::guid() /
::opaque() factories on the OPC UA client side.
Variant
Scalar Double: {"type": 11, "value": 42.5, "dimensions": null}
Int32 array: {"type": 6, "value": [1, 2, 3], "dimensions": null}
2-D Double: {"type": 11, "value": [1.0, 2.0, 3.0, 4.0], "dimensions": [2, 2]}
| Field | Meaning |
|---|---|
type |
BuiltinType integer value (1-25) |
value |
PHP-native value — scalar, array, or nested Variant |
dimensions |
Array of integers for multi-dimensional arrays; null otherwise |
DataValue
{
"value": 42.5,
"type": 11,
"dimensions": null,
"statusCode": 0,
"sourceTimestamp": "2026-05-15T10:30:00.000000+00:00",
"serverTimestamp": "2026-05-15T10:30:00.123456+00:00"
}
The DataValue payload inlines the Variant fields
(value, type, dimensions) — it does not nest a Variant
object. This makes the common-case read response one less level of
indentation.
Timestamps use ISO 8601 with microseconds and explicit UTC offset.
Bad-status DataValues still carry value and type; check
statusCode before trusting the value.
QualifiedName
{"ns": 2, "name": "Speed"}
LocalizedText
{"locale": "en-US", "text": "Conveyor Speed"}
locale may be null for locale-less texts.
ReferenceDescription
Returned by browse and friends.
{
"referenceTypeId": {"ns": 0, "id": 47, "type": "numeric"},
"isForward": true,
"nodeId": {"ns": 2, "id": "Devices/PLC1", "type": "string"},
"browseName": {"ns": 2, "name": "PLC1"},
"displayName": {"locale": "en", "text": "PLC 1"},
"nodeClass": 1,
"typeDefinition": {"ns": 0, "id": 58, "type": "numeric"}
}
nodeClass is the NodeClass enum's integer value (1 = Object,
2 = Variable, 4 = Method, …).
BrowseNode
Returned by browseRecursive. Recursive shape with a reference
and a children array.
{
"reference": {/* ReferenceDescription as above */},
"children": [
{"reference": {...}, "children": [...]},
{"reference": {...}, "children": []}
]
}
EndpointDescription
Returned by getEndpoints.
{
"endpointUrl": "opc.tcp://plc.local:4840",
"serverCertificate": "<base64 DER>",
"securityMode": 3,
"securityPolicyUri": "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256",
"userIdentityTokens": [
{"policyId": "anonymous", "tokenType": 0, "issuedTokenType": null, "issuerEndpointUrl": null, "securityPolicyUri": null}
],
"transportProfileUri": "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary",
"securityLevel": 50
}
Module result DTOs
Service-set responses serialise as plain object shapes. The fields mirror the PHP DTO property-by-property:
| DTO | Shape |
|---|---|
SubscriptionResult |
{subscriptionId, revisedPublishingInterval, revisedLifetimeCount, revisedMaxKeepAliveCount} |
MonitoredItemResult |
{statusCode, monitoredItemId, revisedSamplingInterval, revisedQueueSize} |
MonitoredItemModifyResult |
{statusCode, revisedSamplingInterval, revisedQueueSize} |
CallResult |
{statusCode, inputArgumentResults, outputArguments} — outputArguments is Variant[] |
PublishResult |
{subscriptionId, sequenceNumber, moreNotifications, notifications, availableSequenceNumbers} |
DataChangeNotification |
{type: "DataChange", clientHandle, dataValue} — one entry of PublishResult.notifications |
EventNotification |
{type: "Event", clientHandle, eventFields} — eventFields is Variant[] |
TransferResult |
{statusCode, availableSequenceNumbers} |
BrowsePathResult |
{statusCode, targets} — targets is BrowsePathTarget[] |
BrowsePathTarget |
{targetId, remainingPathIndex} |
BrowseResultSet |
{references, continuationPoint} — references is ReferenceDescription[] |
AddNodesResult |
{statusCode, addedNodeId} — addedNodeId is NodeId |
BuildInfo |
{productName, manufacturerName, softwareVersion, buildNumber, buildDate} |
SetTriggeringResult |
{addResults, removeResults} |
Nested types follow their own shape rules — a CallResult.outputArguments[0]
is a Variant object as documented above.
PublishResult.notifications is a heterogeneous list: each entry is either
a DataChangeNotification or an EventNotification, told apart by the
type discriminator. Since opcua-client v4.5.0 both are typed PHP objects
rather than associative arrays — the wire shape above is unchanged, so
ManagedClient and daemon can be on either side of that bump.
Enums
Enums are serialised by their integer value (backed enums) or by their case name (pure enums):
| Enum | Wire form |
|---|---|
BuiltinType |
int (1-25) |
NodeClass |
int (1, 2, 4, 8, 16, 32, 64, 128) |
BrowseDirection |
int (0 = Forward, 1 = Inverse, 2 = Both) |
ConnectionState |
string ("Disconnected", "Connected", "Broken") |
The client-side deserialiser (TypeSerializer::deserializeXxx)
handles the inverse mapping.
DateTimeImmutable
ISO 8601 with microseconds and explicit UTC offset:
"2026-05-15T10:30:00.000000+00:00".
The serialiser always emits UTC. Times from the OPC UA wire (which
are FILETIME ticks since 1601-01-01 UTC) are converted to PHP
DateTimeImmutable and re-encoded to ISO before serialisation.
ExtensionObject
{
"typeId": {"ns": 2, "id": 5001, "type": "numeric"},
"encoding": 1,
"body": "<base64-encoded bytes, when raw>",
"value": null
}
When the daemon has a codec registered for the typeId, body is
null and value carries the decoded structured payload.
Programmatic use
You can use TypeSerializer directly when implementing custom
deserialisers or debugging:
use PhpOpcua\SessionManager\Serialization\TypeSerializer;
use PhpOpcua\Client\Types\NodeId;
$serializer = new TypeSerializer();
$wire = $serializer->serialize(NodeId::numeric(0, 2261));
// → ['ns' => 0, 'id' => 2261, 'type' => 'numeric']
$back = $serializer->deserializeNodeId($wire);
// → NodeId(ns=0;i=2261)
The serializer holds no state and is safe to share across calls.
Round-trip safety
Every type listed above round-trips losslessly:
- Numerics through JSON's number type (with the caveat that very
large UInt64 values that exceed
PHP_INT_MAXare not supported by JSON natively). - Strings as JSON strings (UTF-8).
- Byte strings as base64-encoded strings.
- DateTimeImmutable as ISO with microseconds.
The Float type loses precision compared to PHP's native double —
that is the OPC UA spec's problem, not the serialiser's.