# Data types & features across the wire
This is the reference for what crosses the Java ↔ Node.js boundary intact, and how each capability looks in both languages. Every example is trimmed from a runnable, test-verified integration demo, so the shapes shown here are exactly what survives the round trip.
On the wire there is just JSON, so the rule is simple: stick to JSON-safe values and everything — primitives, arrays, nested objects, events, cached results, metadata — round-trips identically. Binary data travels on a separate streaming channel.
On the Java side these values are built and read through the io.datatree.Tree API, the equivalent of a
JavaScript object (see the DataTree API).
AVOID NON-JSON TYPES
Do not put language-specific values — JavaScript Date, Map, Set, BigInt, Buffer, or Java
Date/byte[] — inside action params or responses. The native JSON serializer used on both sides
cannot represent them identically, so they would not survive the round trip. For binary data, use
Binary streaming instead of embedding bytes in JSON.
| Capability | Crosses intact | Section |
|---|---|---|
| Primitives (int, large int, double, boolean, string, null) | ✓ | Primitives |
| Arrays / lists (incl. nested) | ✓ | Lists |
| Objects / maps (nested) | ✓ | Maps |
| Deeply nested, life-like structures | ✓ | Complex structures |
Events (emit and broadcast) | ✓ | Events |
| Cached action results (with TTL) | ✓ | Caching |
| Request/response metadata | ✓ | Metadata |
| Errors & exceptions (typed, structured) | ✓ | Errors & exceptions |
| Binary data | ✓ (streaming) | Binary streaming |
# Primitives
Every JSON-safe primitive comes back with the same type and value. Note that large integers are safe up
to JavaScript's Number.MAX_SAFE_INTEGER (9007199254740991), and null is a real JSON null on both
sides.
# Lists (arrays)
A list — including lists of objects with their own nested arrays — round-trips as a JSON array. In Java
you build it with putList(...) / addMap(); a caller reads it back as an array.
# Maps (nested objects)
Nested keys and values survive unchanged. putMap(...) on the Java side is the equivalent of a nested
JavaScript object.
# Deep echo
The strongest single proof that structured data crosses intact: an echo action that returns its input
unchanged. A caller sends a rich nested object and gets back a deep-equal copy.
# Complex nested structures
A life-like object — a list of users, each with a nested address (holding a geo sub-object), arrays
of strings (emails, roles) and an array of objects (phones) — crosses with full deep equality. This
is also the clearest side-by-side of the Tree API and a JavaScript object (one user shown; the demo
sends two).
# Events
Events cross the language boundary too, and the two delivery modes behave the same way across languages:
emitis load-balanced — one listener per group receives the event.broadcastis delivered to every listener on every node.
A service subscribes with an event listener; any node sends with emit or broadcast. (For listener
groups, see Events.)
Subscribing:
Sending:
# Caching with TTL
Caching happens on the node that owns the action, so a remote caller in the other language benefits
from the cache too. The result is cached by the listed key(s) for ttl seconds: two calls with the same
key within the TTL return the cached value (the body does not run again), a different key is a different
cache entry, and after the TTL the entry is evicted and the body runs again. (See
Caching for cacher options.)
# Metadata
Metadata is the one wire field with two APIs. It rides alongside the params and is merged back into the caller with the response — but you reach it differently in each language:
- Node.js:
ctx.meta - Java:
ctx.params.getMeta()
Both serialize to the request's top-level meta field, so they are fully interchangeable. Keep meta
values JSON-safe.
Reading and answering metadata in a service:
Sending metadata on a call, and reading what came back:
The most common real use is one-directional: a caller passes cross-cutting context (tenant, auth
token, locale) in meta — never in params — and the remote service only reads it. Here a Node.js
caller scopes a Java service by tenant:
# Errors & exceptions
An error is not just a string on the wire — it crosses as a structured object, so the calling side
in the other language gets a typed exception with the same fields, not a flattened message. When a Java
action throws (or a Promise rejects), moleculer-java serializes the error into the response packet;
the Node.js caller receives a Moleculer error with the matching properties, and vice versa.
These are the fields that travel (the Java classes live in services.moleculer.error):
| Wire field | Node.js (err.…) | Java (MoleculerError) | Meaning |
|---|---|---|---|
name | err.name | getName() | The error class — the cross-language discriminator used to rebuild the right type. |
message | err.message | getMessage() | Human-readable message. |
code | err.code | getCode() | HTTP-like status code (e.g. 422, 500). |
type | err.type | getType() | Machine-readable type string you choose (e.g. "TENANT_MISSING"). |
data | err.data | getData() → Tree | Arbitrary JSON detail payload. |
retryable | err.retryable | isRetryable() | Whether the caller's retry logic may retry the call. |
nodeID | err.nodeID | originating node id | Which node produced the error. |
stack | err.stack | getStack() | Stack trace, as a string. |
Java throws → Node.js catches. The Java side throws a typed error; the Node.js caller reads the same fields back:
Node.js throws → Java catches. A built-in Node.js Moleculer error is rebuilt as the matching Java class on arrival:
A few rules that are pure protocol behavior — you cannot guess them, so rely on them:
nameis the discriminator. moleculer-java maps a knownnameback to the matching Java class (MoleculerError,MoleculerRetryableError,MoleculerServerError,MoleculerClientError,ValidationError(422),ServiceNotFoundError,RequestTimeoutError, …). An unknownname(e.g. a custom Node.js error class) arrives as a genericMoleculerErrorthat still carries all the fields above. Node.js applies the same fall-back in the other direction.- A plain Java exception is auto-wrapped. If something that is not a
MoleculerErrorescapes an action, moleculer-java wraps it as a genericMoleculerError(name"MoleculerError",code500,type"UNKNOWN_ERROR",retryablefalse) before sending — the other side never receives a raw Java stack, always the structured shape. retryabledrives retries. The caller's retry logic (see Fault tolerance andCallOptions.retryCount) only retries calls that failed with aretryable = trueerror; throw aMoleculerRetryableError(or setretryable) when a retry could succeed.
# Binary streaming
Binary data does not travel as JSON params — it travels on Moleculer's streaming channel. A
streamed request opens with empty params; any side-data (such as a filename) must travel in meta,
never in params, or the receiver would treat the first packet as stream content.
Receiving a stream (an action that consumes incoming bytes):
Sending a stream (the caller opens a stream and pushes bytes; note: stream only, no params):
Producing a stream (an action that returns bytes for the caller to download):
# See also
- Call Java from Node.js and Call Node.js from Java — the two directional guides.
- Moleculer concepts — the
TreeAPI in depth.
Every snippet on this page is trimmed from a runnable, test-verified integration demo where a Java node and a Node.js node exchange exactly these shapes and assert they arrive intact in both directions.