ToJson
A table becomes JSON text: an object, with arrays as lists and strings as strings.
input
out
- Type name
signal:ToJson- Plane
signal- Tags
transformtext- Language
rust- Tier
native- Bundle
signal- Source
node-bundles/signal/ToJson.rs- Availability
- available
Slots
| Slot | Direction | Type | |
|---|---|---|---|
input | input | TABLE | |
out | output | STRING |
Parameters
json
| Name | Type | Default | Range | Doc |
|---|---|---|---|---|
indent | int | 0 | 0 … 8 | Spaces of indent per level. 0 writes the whole table on one line. |
decimals | int | -1 | -1 … 17 | Digits after the point on every number. -1 writes each one in full. |
common
| Name | Type | Default | Range | Doc |
|---|---|---|---|---|
autotrigger | bool | false | Run on the node's own schedule, instead of waiting for an input frame. Turn this on for sources; leave it off for transforms driven by their input. | |
max_frequency | float | 0 | 0 … 100 | Rate cap for this node, read through `frequency_mode`. 0 means uncapped — the node runs as often as the scheduler and its inputs allow. |
frequency_mode | string | updates_per_second | updates_per_second | seconds_per_update | How to read `max_frequency`: as a rate in Hz (updates per second), or as a period in seconds between updates — convenient for very slow nodes. |
Source
The current source of this node, as it stands in the goofi repository at node-bundles/signal/ToJson.rs.
//! ToJson — a table becomes JSON text. A table is an object, an array a list, a string a string.
use goofi_core::{Data, Meta, SlotType, Value};
use goofi_signal_sdk::{Inputs, Manifest, Node, NodeCtx, NodeResult, OutputDecl, Outputs, ParamDecl, Params, ParamSpec, SlotDecl, Tag};
#[derive(Default)]
struct ToJson;
fn quote(s: &str, out: &mut String) {
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
}
fn number(v: f32, decimals: i64, out: &mut String) {
if !v.is_finite() {
// JSON has no infinity and no NaN; null is what every reader takes for a missing number.
out.push_str("null");
} else if decimals < 0 {
out.push_str(&format!("{v}"));
} else {
out.push_str(&format!("{v:.*}", decimals as usize));
}
}
struct Writer {
indent: usize,
decimals: i64,
}
impl Writer {
fn newline(&self, depth: usize, out: &mut String) {
if self.indent > 0 {
out.push('\n');
out.push_str(&" ".repeat(self.indent * depth));
}
}
/// The array as nested lists: one level of list per axis, in the shape's own order.
fn rows(&self, shape: &[usize], values: &[f32], depth: usize, out: &mut String) {
let Some((&span, rest)) = shape.split_first() else {
return number(values[0], self.decimals, out);
};
let step = values.len().checked_div(span.max(1)).unwrap_or(0);
out.push('[');
for i in 0..span {
if i > 0 {
out.push(',');
if self.indent == 0 {
out.push(' ');
}
}
self.newline(depth + 1, out);
self.rows(rest, &values[i * step..(i + 1) * step], depth + 1, out);
}
if span > 0 {
self.newline(depth, out);
}
out.push(']');
}
fn value(&self, d: &Data, depth: usize, out: &mut String) {
match d.value() {
Value::Str(s) => quote(s, out),
Value::Array(a) => {
let values: Vec<f32> = a
.as_bytes()
.chunks_exact(4)
.map(|b| f32::from_le_bytes(b.try_into().expect("four bytes")))
.collect();
self.rows(a.shape(), &values, depth, out);
}
Value::Table(t) => {
out.push('{');
for (i, (k, v)) in t.iter().enumerate() {
if i > 0 {
out.push(',');
if self.indent == 0 {
out.push(' ');
}
}
self.newline(depth + 1, out);
quote(k, out);
out.push(':');
out.push(' ');
self.value(v, depth + 1, out);
}
if !t.is_empty() {
self.newline(depth, out);
}
out.push('}');
}
}
}
}
impl Node for ToJson {
fn process(
&mut self,
inp: &Inputs<'_>,
out: &mut Outputs<'_>,
_c: &mut NodeCtx,
p: &Params<'_>,
) -> NodeResult {
let d = inp.get("input").ok_or("`input` is required")?;
d.as_table()?;
let w = Writer {
indent: p.i64("json", "indent").unwrap_or(0).clamp(0, 8) as usize,
decimals: p.i64("json", "decimals").unwrap_or(-1).clamp(-1, 17),
};
let mut text = String::new();
w.value(d, 0, &mut text);
out.set("out", Data::string(text, Meta::new()));
Ok(())
}
}
static PARAMS: &[ParamDecl] = &[
ParamDecl {
group: "json",
name: "indent",
spec: ParamSpec::Int { default: 0, min: 0, max: 8 },
expression: None,
doc: Some("Spaces of indent per level. 0 writes the whole table on one line."),
},
ParamDecl {
group: "json",
name: "decimals",
spec: ParamSpec::Int { default: -1, min: -1, max: 17 },
expression: None,
doc: Some("Digits after the point on every number. -1 writes each one in full."),
},
];
static INPUTS: &[SlotDecl] = &[SlotDecl {
name: "input",
kind: SlotType::Table,
trigger_process: true,
multi: false,
required: true,
}];
static OUTPUTS: &[OutputDecl] = &[OutputDecl { name: "out", kind: SlotType::String }];
static MANIFEST: Manifest = Manifest {
tags: &[Tag::Transform, Tag::Text],
doc: "A table becomes JSON text: an object, with arrays as lists and strings as strings.",
inputs: INPUTS,
outputs: OUTPUTS,
params: PARAMS,
producer: false,
};
goofi_signal_sdk::export!(ToJson, MANIFEST);This reference describes goofi 3.1.0(537cd394), generated from a running instance on 2026-09-06.