diff --git a/Opc.Ua.JsonNodeSet/AddressSpace.Variants.cs b/Opc.Ua.JsonNodeSet/AddressSpace.Variants.cs
new file mode 100644
index 0000000..ee70861
--- /dev/null
+++ b/Opc.Ua.JsonNodeSet/AddressSpace.Variants.cs
@@ -0,0 +1,372 @@
+using Newtonsoft.Json.Linq;
+using Opc.Ua.JsonNodeSet.Model;
+
+namespace Opc.Ua.JsonNodeSet;
+
+///
+/// Variant canonicalization for .
+///
+/// Responsibilities:
+///
+/// - Maintain indexes that allow schema-aware Variant emission:
+/// _encodingToDataType, _dataTypeToXmlEncoding, _modelXmlNamespace.
+/// - Strip redundant "Default JSON" encoding nodes on request.
+/// - : rewrite every 's Variant DOM so
+/// that structure keys are in order,
+/// normalize XML encoding TypeIds to DataType NodeIds, and validate unions.
+///
+///
+public partial class AddressSpace
+{
+ private const string HasEncodingRefId = "i=38";
+ private const string DefaultXmlBrowseName = "Default XML";
+ private const string DefaultJsonBrowseName = "Default JSON";
+ private const string DefaultBinaryBrowseName = "Default Binary";
+
+ // encoding NodeId → DataType NodeId
+ private Dictionary? _encodingToDataType;
+ // DataType NodeId → "Default XML" encoding NodeId
+ private Dictionary? _dataTypeToXmlEncoding;
+ // model ModelUri → XmlSchemaUri
+ private Dictionary? _modelXmlNamespace;
+ // XmlSchemaUri → ModelUri (reverse)
+ private Dictionary? _xmlNamespaceToModel;
+ // DataType BrowseName (bare) → DataType NodeId (first match wins)
+ private Dictionary? _dataTypeBrowseNameIndex;
+
+ ///
+ /// (Re)builds the variant-related indexes. Called lazily from the Try... lookups, and
+ /// invoked explicitly by . Cheap to call — O(nodes).
+ ///
+ public void BuildVariantIndexes()
+ {
+ _encodingToDataType = new Dictionary(StringComparer.Ordinal);
+ _dataTypeToXmlEncoding = new Dictionary(StringComparer.Ordinal);
+ _modelXmlNamespace = new Dictionary(StringComparer.Ordinal);
+ _xmlNamespaceToModel = new Dictionary(StringComparer.Ordinal);
+ _dataTypeBrowseNameIndex = new Dictionary(StringComparer.Ordinal);
+
+ // Model XML namespace map: prefer explicit XmlSchemaUri, otherwise synthesize
+ // "{ModelUri}/Types.xsd" (the common convention). Warnings could be routed through
+ // a logger if one existed; for now we synthesize silently.
+ foreach (var kvp in _models)
+ {
+ var uri = kvp.Key;
+ var xmlNs = !string.IsNullOrEmpty(kvp.Value.XmlSchemaUri)
+ ? kvp.Value.XmlSchemaUri!
+ : uri.TrimEnd('/') + "/Types.xsd";
+ _modelXmlNamespace[uri] = xmlNs;
+ _xmlNamespaceToModel[xmlNs] = uri;
+ }
+
+ // HasEncoding references: source is DataType, target is encoding Object node with
+ // BrowseName "Default XML" / "Default JSON" / "Default Binary".
+ foreach (var node in _sequence)
+ {
+ if (node is UADataType dt && node.NodeId != null)
+ {
+ var bn = StripNamespacePrefix(node.BrowseName);
+ if (!string.IsNullOrEmpty(bn) && !_dataTypeBrowseNameIndex.ContainsKey(bn!))
+ _dataTypeBrowseNameIndex[bn!] = node.NodeId;
+ }
+
+ if (!_forwardRefs.TryGetValue(node.NodeId ?? "", out var fwds)) continue;
+ foreach (var entry in fwds)
+ {
+ if (entry.ReferenceTypeId != HasEncodingRefId) continue;
+ if (!_nodes.TryGetValue(entry.TargetNodeId, out var encodingNode)) continue;
+ var encBn = StripNamespacePrefix(encodingNode.BrowseName);
+ if (encBn == null) continue;
+
+ _encodingToDataType[entry.TargetNodeId] = node.NodeId!;
+ if (string.Equals(encBn, DefaultXmlBrowseName, StringComparison.Ordinal))
+ _dataTypeToXmlEncoding[node.NodeId!] = entry.TargetNodeId;
+ // Default JSON / Default Binary recorded in _encodingToDataType so reader-side
+ // normalization still works, but only XML needs the reverse map.
+ }
+ }
+ }
+
+ private static string? StripNamespacePrefix(string? value)
+ {
+ if (string.IsNullOrEmpty(value)) return value;
+ if (!value.StartsWith("nsu=", StringComparison.Ordinal)) return value;
+ var semi = value.IndexOf(';');
+ return semi > 0 ? value.Substring(semi + 1) : value;
+ }
+
+ private void EnsureVariantIndexes()
+ {
+ if (_encodingToDataType == null) BuildVariantIndexes();
+ }
+
+ ///
+ /// If is an encoding NodeId (Default XML/JSON/Binary), returns
+ /// the underlying DataType NodeId. Otherwise returns unchanged
+ /// (or null when input is null).
+ ///
+ public string? NormalizeEncodingNodeIdToDataType(string? nodeId)
+ {
+ if (nodeId == null) return null;
+ EnsureVariantIndexes();
+ if (_nodes.TryGetValue(nodeId, out var node) && node is UADataType) return nodeId;
+ if (_encodingToDataType!.TryGetValue(nodeId, out var dt)) return dt;
+ return nodeId;
+ }
+
+ internal string? TryGetXmlEncodingNodeId(string dataTypeNodeId)
+ {
+ EnsureVariantIndexes();
+ return _dataTypeToXmlEncoding!.TryGetValue(dataTypeNodeId, out var enc) ? enc : null;
+ }
+
+ internal string? TryGetXmlSchemaUriForNodeId(string dataTypeNodeId)
+ {
+ EnsureVariantIndexes();
+ var ns = ExtractNsu(dataTypeNodeId);
+ if (ns == null) return null;
+ return _modelXmlNamespace!.TryGetValue(ns, out var xmlNs) ? xmlNs : null;
+ }
+
+ internal string? TryGetXmlSchemaUriForBrowseName(string browseName)
+ {
+ EnsureVariantIndexes();
+ if (!_dataTypeBrowseNameIndex!.TryGetValue(browseName, out var nodeId)) return null;
+ return TryGetXmlSchemaUriForNodeId(nodeId);
+ }
+
+ internal string? TryGetDataTypeBrowseName(string dataTypeNodeId)
+ {
+ if (!_nodes.TryGetValue(dataTypeNodeId, out var node)) return null;
+ return StripNamespacePrefix(node.BrowseName);
+ }
+
+ internal UADataType? ResolveDataType(string? typeId)
+ {
+ if (typeId == null) return null;
+ EnsureVariantIndexes();
+ if (_nodes.TryGetValue(typeId, out var n) && n is UADataType dt) return dt;
+ if (_encodingToDataType!.TryGetValue(typeId, out var real)
+ && _nodes.TryGetValue(real, out var n2) && n2 is UADataType dt2) return dt2;
+ return null;
+ }
+
+ ///
+ /// Remove "Default JSON" encoding objects (and the HasEncoding references pointing at
+ /// them) from the AddressSpace. The JSON encoding of a Variant always uses the DataType
+ /// NodeId directly, so these nodes carry no information.
+ ///
+ public int StripDefaultJsonEncodings()
+ {
+ var toRemove = new List();
+ foreach (var node in _sequence)
+ {
+ if (node.NodeClass != NodeClass.UAObject) continue;
+ var bn = StripNamespacePrefix(node.BrowseName);
+ if (!string.Equals(bn, DefaultJsonBrowseName, StringComparison.Ordinal)) continue;
+ // Confirm it really is a HasEncoding target of a DataType.
+ if (node.NodeId != null &&
+ _inverseRefs.TryGetValue(node.NodeId, out var inv) &&
+ inv.Any(e => e.ReferenceTypeId == HasEncodingRefId))
+ {
+ toRemove.Add(node.NodeId);
+ }
+ }
+ foreach (var id in toRemove) RemoveNode(id);
+ if (toRemove.Count > 0) BuildVariantIndexes();
+ return toRemove.Count;
+ }
+
+ ///
+ /// Canonicalize every 's Variant DOM:
+ /// reorder struct keys to match
+ /// field order, normalize any XML encoding TypeIds encountered in ExtensionObjects
+ /// to their underlying DataType NodeIds, and validate unions. Idempotent.
+ ///
+ public void ResolveVariants()
+ {
+ BuildVariantIndexes();
+
+ foreach (var node in _sequence)
+ {
+ if (node is not UAVariable v) continue;
+ if (v.Value?.Value == null) continue;
+
+ var dtId = NormalizeEncodingNodeIdToDataType(v.DataType);
+ v.Value.Value = CanonicalizeVariantValue(v.Value.Value, dtId, isArray: v.ValueRank is > 0);
+ }
+ }
+
+ private object? CanonicalizeVariantValue(object? value, string? dataTypeNodeId, bool isArray)
+ {
+ // Scalars pass through unchanged.
+ if (value is null) return null;
+
+ // Array: canonicalize each item with the element DataType.
+ if (value is JArray arr)
+ {
+ for (int i = 0; i < arr.Count; i++)
+ {
+ arr[i] = CanonicalizeJsonToken(arr[i], dataTypeNodeId) ?? JValue.CreateNull();
+ }
+ return arr;
+ }
+ if (value is List