npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

node-opcua-uanodeset-json

v2.186.12

Published

pure nodejs OPCUA SDK - the Annex I JSON NodeSet serialisations (OPC 10000-6 Annex I)

Readme

node-opcua-uanodeset-json

The JSON NodeSet of OPC 10000-6 Annex I, in all three of its serialisations, readable and writable by node-opcua.

see http://node-opcua.github.io/

npm install node-opcua-uanodeset-json

Why

An information model has been a NodeSet2 XML document for as long as OPC UA has had one. Annex I is the normative JSON form of the same thing, and it is what you want when the consumer is not an OPC UA stack: a browser, a jq pipeline, a document database, a diff in a code review, or a build step that has to add one node to a companion specification without a full XML toolchain.

The annex specifies three shapes of the same model, and this package implements all three:

| extension | Annex | what it is | reach for it when | |---|---|---|---| | .jsonl | I.4 | one JSON object per line, header first | you want to stream, grep, or diff a model line by line | | .json | I.1 | one document, children nested inside their parent | you want to read it, or hand it to a JSON API | | .uanodeset | I.3 | a TAR.GZ of a manifest and numbered members | the model is large enough that one file is unwieldy |

Reading: nothing to call

Importing the package registers all three with the address-space loader, so a path is enough:

import { AddressSpace } from "node-opcua-address-space";
import { generateAddressSpace } from "node-opcua-address-space/nodeJS";
import "node-opcua-uanodeset-json";

const addressSpace = AddressSpace.create();
await generateAddressSpace(addressSpace, [
    "Opc.Ua.NodeSet2.xml",     // still XML
    "Opc.Ua.Di.jsonl",         // Annex I line form
    "MyCompanionSpec.uanodeset" // Annex I archive
]);

node-opcua-server already imports it, so a server gets this for free. Formats are chosen by sniffing the content, not the extension, and the head a sniffer is shown is already inflated, so .jsonl.gz and .json.gz work as well and a misnamed file still loads.

To register only one form, import only that subpath. It is the point of the subpaths:

import "node-opcua-uanodeset-json/jsonl";

Reading is exact, and that claim is tested rather than asserted. The DI nodeset loaded from .jsonl, .json and .uanodeset produces an address space with the same digest as the one loaded from its NodeSet2 XML, where the digest hashes every node's id, browse name, class, reference count and, for variables, status code and value. Agreeing node counts would not be that claim.

Writing

The writers take a stream of NodesetRecord, the intermediate form the loader produces, so converting between any two formats is reading one and writing the other:

import { writeFile } from "node:fs/promises";
import { recordsToAnnexIJson } from "node-opcua-uanodeset-json";

await writeFile("Opc.Ua.Di.json", recordsToAnnexIJson(records), "utf8");
import zlib from "node:zlib";
import { recordsToAnnexIArchiveTar } from "node-opcua-uanodeset-json";

const tar = recordsToAnnexIArchiveTar(records, { maxNodesPerFile: 1000 });
await writeFile("Opc.Ua.Di.uanodeset", zlib.gzipSync(Buffer.from(tar)));

Writing is a fixpoint: write(read(write(read(x)))) is byte-identical to write(read(x)), for the document form and for the archive down to the tar bytes. Splitting an archive never divides a subtree across two members, and the members carry no Models, because I.3 says the manifest specifies them.

Cross-implementation, which is the check that actually matters: the OPC Foundation's UA-NodeSetTool reads what this writes and its compare reports the result identical to the original XML.

What the annex does differently

Three decisions in I.1 will surprise a reader who knows NodeSet2 XML, and each one is why a naive translation does not work:

  • No namespace table. A NodeId embeds its namespace URI: nsu=http://…/DI/;i=1002, and a QualifiedName likewise. Nothing is relative to a document-scoped index, so a node can be moved between documents unchanged. parseAnnexINodeId and formatAnnexINodeId are the conversion, and they split on the first semicolon, since a URI may contain one.
  • No aliases. Reference types are written out.
  • Three references are not references. HasTypeDefinition becomes the TypeId field, HasModellingRule becomes ModellingRuleId, and the forward edge from a parent to its child is implied by ParentId. A reader that only walks References silently loses all three; this package puts them back.

The document form adds two more:

  • Nodes are partitioned into eight containers by NodeClass, and children nest inside their parent's ChildList rather than sitting at the top level.
  • Ordered and Declarations handle cycles. I.2 makes reading order normative so a decoder reading forwards never meets a NodeId it has not already seen, and Declarations carries stubs for the ones that cannot be ordered. A test model exists specifically to exercise this, because it is what breaks a naive reader.

API

Most callers need only the side-effect import. The rest is for building tools.

| export | what it is | |---|---| | recordsToAnnexIJson(records) | records as an Annex I document, serialised | | recordsToAnnexIDocument(records) | the same as an AnnexIUANodeSet object | | recordsToAnnexIArchiveTar(records, options?) | records as the tar bytes of an archive, before gzip | | flattenAnnexIDocument(nodeSet) | a document's nodes in I.2 reading order, un-nested, with ParentId filled in | | annexIHeaderRecord / annexINodeRecord | the codec: Annex I to NodesetRecord | | decodeAnnexIVariant, AnnexIValueError | a Part 6 1.05 JSON Variant, as Annex I carries it | | parseAnnexINodeId, formatAnnexINodeId, and the QualifiedName pair | the nsu= canonical forms | | annexINamespaceTable, OPCUA_CORE_NAMESPACE | namespace resolution | | isAnnexIHeaderLine, isAnnexIDocumentHead, isAnnexIArchiveHead | the three sniffers, usable directly | | readTar, writeTar, isTar, TarEntry | the tar layer | | ANNEX_I_JSONL_FORMAT, ANNEX_I_JSON_FORMAT, ANNEX_I_ARCHIVE_FORMAT | format names, for nodesetFormatByName | | registerAnnexIJsonlFormat() and friends | explicit registration; idempotent, and the imports already call them | | AnnexINode, AnnexIUANodeSet, AnnexIReference, … | the model types |

The tar layer

.uanodeset is a TAR.GZ, and the monorepo has no tar dependency. This should not be the package that adds one, so readTar / writeTar are about 150 lines here: they read both the original V7 layout and ustar, and write ustar.

isTar recognises an archive by its header checksum rather than by a magic string, because V7 leaves the magic empty and that is what the reference implementation writes. A checksum is the only structural test that works on both.

Archives are validated against I.3 rather than read optimistically. A manifest naming a file the archive lacks, and a file the archive holds that the manifest does not name, are both refused: either one silently changes which nodes get loaded.

License

MIT