Skip to content

Commit 525fba1

Browse files
Add JSON-Safe serialization modes (#1308)
## **Public API Changes** **New subscription option:** - `serializationMode: 'default' | 'plain' | 'json'` for `createSubscription()` **New utility functions:** - `rclnodejs.toJSONSafe(obj)` - makes objects JSON-friendly - `rclnodejs.toJSONString(obj)` - converts directly to JSON string **Fully backward compatible** - existing code works unchanged. ## **Description** This adds **message serialization modes** to fix a common headache: ROS sensor messages use `TypedArray`s which don't play nice with JSON. **The Problem:** If you try to `JSON.stringify()` a LaserScan message, you get `{}` instead of the actual data. This breaks web APIs, logging, and any workflow that needs JSON. **The Solution:** Three modes to handle messages differently: - **`'default'`** Uses native rclnodejs behavior - **`'plain'`**: Convert TypedArrays to regular arrays - JSON works! - **`'json'`**: Handle everything (TypedArrays, BigInt, NaN) - fully JSON-safe [#1307](#1307)
1 parent c8f0ae6 commit 525fba1

File tree

10 files changed

+539
-2
lines changed

10 files changed

+539
-2
lines changed

example/topics/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,26 @@ The `subscriber/` directory contains examples of nodes that subscribe to topics:
146146
- **Features**: ROS 2 service introspection capabilities
147147
- **Run Command**: `node subscriber/subscription-service-event-example.js`
148148

149+
### 8. Serialization Modes Subscriber (`subscription-serialization-modes-example.js`)
150+
151+
**Purpose**: Demonstrates different serialization modes for message handling.
152+
153+
- **Message Type**: `sensor_msgs/msg/LaserScan`
154+
- **Topic**: `scan`
155+
- **Functionality**: Shows how 'default', 'plain', and 'json' modes affect message serialization
156+
- **Features**: Message serialization control for web applications and JSON compatibility
157+
- **Run Command**: `node subscriber/subscription-serialization-modes-example.js`
158+
159+
### 9. JSON Utilities Subscriber (`subscription-json-utilities-example.js`)
160+
161+
**Purpose**: Demonstrates manual message conversion utilities.
162+
163+
- **Message Type**: `sensor_msgs/msg/LaserScan`
164+
- **Topic**: `scan`
165+
- **Functionality**: Shows how to use toJSONSafe and toJSONString utilities for manual conversion
166+
- **Features**: Manual conversion of TypedArrays, BigInt, and special values for JSON serialization
167+
- **Run Command**: `node subscriber/subscription-json-utilities-example.js`
168+
149169
## Validator Example
150170

151171
The `validator/` directory contains validation utilities:
@@ -193,6 +213,7 @@ Several examples work together to demonstrate complete communication:
193213
- **Raw Messages**: Binary data transmission
194214
- **Service Events**: Monitoring service interactions
195215
- **Multi-dimensional Arrays**: Complex data structures with layout information
216+
- **Message Serialization**: TypedArray handling and JSON-safe conversion for web applications
196217
- **Validation**: Name and topic validation utilities
197218

198219
## Notes
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) 2025 Mahmoud Alghalayini. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
const rclnodejs = require('../../../index.js');
18+
19+
/**
20+
* This example demonstrates the JSON utility functions for manual message conversion.
21+
* These utilities are useful when you need to convert messages on-demand
22+
* rather than using the serializationMode subscription option.
23+
*/
24+
async function main() {
25+
await rclnodejs.init();
26+
const node = new rclnodejs.Node('json_utilities_example_node');
27+
28+
node.createSubscription('sensor_msgs/msg/LaserScan', '/laser_scan', (msg) => {
29+
// Convert using utility functions
30+
const jsonSafe = rclnodejs.toJSONSafe(msg);
31+
const jsonString = rclnodejs.toJSONString(msg);
32+
33+
console.log(
34+
`Original: ${msg.ranges ? msg.ranges.constructor.name : 'undefined'}, JSON-safe: ${jsonSafe.ranges ? jsonSafe.ranges.constructor.name : 'undefined'}, JSON length: ${jsonString.length}`
35+
);
36+
});
37+
38+
node.spin();
39+
}
40+
41+
main().catch(console.error);
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) 2025 Mahmoud Alghalayini. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
const rclnodejs = require('../../../index.js');
18+
19+
/**
20+
* This example demonstrates the use of serialization modes for subscriptions.
21+
* Serialization modes allow you to control how TypedArrays are handled in messages:
22+
* - 'default': Use native rclnodejs behavior (respects enableTypedArray setting)
23+
* - 'plain': Convert TypedArrays to regular arrays
24+
* - 'json': Fully JSON-safe (converts TypedArrays, BigInt, Infinity, etc.)
25+
*/
26+
async function main() {
27+
await rclnodejs.init();
28+
const node = new rclnodejs.Node('serialization_modes_example_node');
29+
30+
// Default mode: 'default' - uses native rclnodejs behavior
31+
node.createSubscription(
32+
'sensor_msgs/msg/LaserScan',
33+
'/laser_scan',
34+
{ serializationMode: 'default' },
35+
(msg) => {
36+
console.log(
37+
`[TYPED] ranges: ${msg.ranges ? msg.ranges.constructor.name : 'undefined'}`
38+
);
39+
}
40+
);
41+
42+
// Plain mode: converts TypedArrays to regular arrays
43+
node.createSubscription(
44+
'sensor_msgs/msg/LaserScan',
45+
'/laser_scan',
46+
{ serializationMode: 'plain' },
47+
(msg) => {
48+
console.log(
49+
`[PLAIN] ranges: ${msg.ranges ? msg.ranges.constructor.name : 'undefined'}`
50+
);
51+
}
52+
);
53+
54+
// JSON mode: fully JSON-safe
55+
node.createSubscription(
56+
'sensor_msgs/msg/LaserScan',
57+
'/laser_scan',
58+
{ serializationMode: 'json' },
59+
(msg) => {
60+
console.log(
61+
`[JSON] ranges: ${msg.ranges ? msg.ranges.constructor.name : 'undefined'}, JSON-safe: ${canStringifyJSON(msg)}`
62+
);
63+
}
64+
);
65+
66+
node.spin();
67+
}
68+
69+
function canStringifyJSON(obj) {
70+
try {
71+
JSON.stringify(obj);
72+
return true;
73+
} catch {
74+
return false;
75+
}
76+
}
77+
78+
main().catch(console.error);

index.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const ActionUuid = require('./lib/action/uuid.js');
4747
const ClientGoalHandle = require('./lib/action/client_goal_handle.js');
4848
const { CancelResponse, GoalResponse } = require('./lib/action/response.js');
4949
const ServerGoalHandle = require('./lib/action/server_goal_handle.js');
50+
const { toJSONSafe, toJSONString } = require('./lib/message_serialization.js');
5051
const {
5152
getActionClientNamesAndTypesByNode,
5253
getActionServerNamesAndTypesByNode,
@@ -538,6 +539,23 @@ let rcl = {
538539
* @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process.
539540
*/
540541
ros2Launch: ros2Launch,
542+
543+
/**
544+
* Convert a message object to be JSON-safe by converting TypedArrays to regular arrays
545+
* and handling BigInt, Infinity, NaN, etc. for JSON serialization.
546+
* @param {*} obj - The message object to convert
547+
* @returns {*} A JSON-safe version of the object
548+
*/
549+
toJSONSafe: toJSONSafe,
550+
551+
/**
552+
* Convert a message object to a JSON string with proper handling of TypedArrays,
553+
* BigInt, and other non-JSON-serializable values.
554+
* @param {*} obj - The message object to convert
555+
* @param {number} [space] - Space parameter for JSON.stringify formatting
556+
* @returns {string} The JSON string representation
557+
*/
558+
toJSONString: toJSONString,
541559
};
542560

543561
const _sigHandler = () => {

lib/message_serialization.js

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright (c) 2025 Mahmoud Alghalayini. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
/**
18+
* Check if a value is a TypedArray
19+
* @param {*} value - The value to check
20+
* @returns {boolean} True if the value is a TypedArray
21+
*/
22+
function isTypedArray(value) {
23+
return ArrayBuffer.isView(value) && !(value instanceof DataView);
24+
}
25+
26+
/**
27+
* Check if a value needs JSON conversion (BigInt, functions, etc.)
28+
* @param {*} value - The value to check
29+
* @returns {boolean} True if the value needs special JSON handling
30+
*/
31+
function needsJSONConversion(value) {
32+
return (
33+
typeof value === 'bigint' ||
34+
typeof value === 'function' ||
35+
typeof value === 'undefined' ||
36+
value === Infinity ||
37+
value === -Infinity ||
38+
(typeof value === 'number' && isNaN(value))
39+
);
40+
}
41+
42+
/**
43+
* Convert a message to plain arrays (TypedArray -> regular Array)
44+
* @param {*} obj - The object to convert
45+
* @returns {*} The converted object with plain arrays
46+
*/
47+
function toPlainArrays(obj) {
48+
if (obj === null || obj === undefined) {
49+
return obj;
50+
}
51+
52+
if (isTypedArray(obj)) {
53+
return Array.from(obj);
54+
}
55+
56+
if (Array.isArray(obj)) {
57+
return obj.map((item) => toPlainArrays(item));
58+
}
59+
60+
if (typeof obj === 'object' && obj !== null) {
61+
const result = {};
62+
for (const key in obj) {
63+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
64+
result[key] = toPlainArrays(obj[key]);
65+
}
66+
}
67+
return result;
68+
}
69+
70+
return obj;
71+
}
72+
73+
/**
74+
* Convert a message to be fully JSON-safe
75+
* @param {*} obj - The object to convert
76+
* @returns {*} The JSON-safe converted object
77+
*/
78+
function toJSONSafe(obj) {
79+
if (obj === null || obj === undefined) {
80+
return obj;
81+
}
82+
83+
if (isTypedArray(obj)) {
84+
return Array.from(obj).map((item) => toJSONSafe(item));
85+
}
86+
87+
if (needsJSONConversion(obj)) {
88+
if (typeof obj === 'bigint') {
89+
// Convert BigInt to string with 'n' suffix to indicate it was a BigInt
90+
return obj.toString() + 'n';
91+
}
92+
if (obj === Infinity) return 'Infinity';
93+
if (obj === -Infinity) return '-Infinity';
94+
if (typeof obj === 'number' && isNaN(obj)) return 'NaN';
95+
if (typeof obj === 'undefined') return null;
96+
if (typeof obj === 'function') return '[Function]';
97+
}
98+
99+
if (Array.isArray(obj)) {
100+
return obj.map((item) => toJSONSafe(item));
101+
}
102+
103+
if (typeof obj === 'object' && obj !== null) {
104+
const result = {};
105+
for (const key in obj) {
106+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
107+
result[key] = toJSONSafe(obj[key]);
108+
}
109+
}
110+
return result;
111+
}
112+
113+
return obj;
114+
}
115+
116+
/**
117+
* Convert a message to a JSON string
118+
* @param {*} obj - The object to convert
119+
* @param {number} [space] - Space parameter for JSON.stringify formatting
120+
* @returns {string} The JSON string representation
121+
*/
122+
function toJSONString(obj, space) {
123+
const jsonSafeObj = toJSONSafe(obj);
124+
return JSON.stringify(jsonSafeObj, null, space);
125+
}
126+
127+
/**
128+
* Apply serialization mode conversion to a message object
129+
* @param {*} message - The message object to convert
130+
* @param {string} serializationMode - The serialization mode ('default', 'plain', 'json')
131+
* @returns {*} The converted message
132+
*/
133+
function applySerializationMode(message, serializationMode) {
134+
switch (serializationMode) {
135+
case 'default':
136+
// No conversion needed - use native rclnodejs behavior
137+
return message;
138+
139+
case 'plain':
140+
// Convert TypedArrays to regular arrays
141+
return toPlainArrays(message);
142+
143+
case 'json':
144+
// Convert to fully JSON-safe format
145+
return toJSONSafe(message);
146+
147+
default:
148+
throw new TypeError(
149+
`Invalid serializationMode: ${serializationMode}. Valid modes are: 'default', 'plain', 'json'`
150+
);
151+
}
152+
}
153+
154+
/**
155+
* Validate serialization mode
156+
* @param {string} mode - The serialization mode to validate
157+
* @returns {boolean} True if valid
158+
*/
159+
function isValidSerializationMode(mode) {
160+
return ['default', 'plain', 'json'].includes(mode);
161+
}
162+
163+
module.exports = {
164+
isTypedArray,
165+
needsJSONConversion,
166+
toPlainArrays,
167+
toJSONSafe,
168+
toJSONString,
169+
applySerializationMode,
170+
isValidSerializationMode,
171+
};

0 commit comments

Comments
 (0)