FromJson

The first object in a piece of text becomes a table.

FromJson rs
input
out
Type name
signal:FromJson
Plane
signal
Tags
transformtext
Language
rust
Tier
native
Bundle
signal
Source
node-bundles/signal/FromJson.rs
Availability
available

Slots

SlotDirectionType
inputinputSTRING
outoutputTABLE

Parameters

common

NameTypeDefaultRangeDoc
autotriggerboolfalseRun 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_frequencyfloat00 … 100Rate cap for this node, read through `frequency_mode`. 0 means uncapped — the node runs as often as the scheduler and its inputs allow.
frequency_modestringupdates_per_secondupdates_per_second | seconds_per_updateHow 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/FromJson.rs.

node-bundles/signal/FromJson.rs
//! FromJson — the first balanced object in a piece of text becomes a table. An object is a table,
//! a string a String, and everything else an Array.

use goofi_core::indexmap::IndexMap;
use goofi_core::{Data, Meta, SlotType};
use goofi_signal_sdk::{Inputs, Manifest, Node, NodeCtx, NodeResult, OutputDecl, Outputs, Params, SlotDecl, Tag};

#[derive(Default)]
struct FromJson;

enum Json {
    Null,
    Bool(bool),
    Num(f64),
    Str(String),
    Arr(Vec<Json>),
    Obj(Vec<(String, Json)>),
}

struct Reader {
    c: Vec<char>,
    at: usize,
}

impl Reader {
    fn space(&mut self) {
        while self.c.get(self.at).is_some_and(|c| c.is_whitespace()) {
            self.at += 1;
        }
    }

    fn peek(&mut self) -> Result<char, String> {
        self.space();
        self.c.get(self.at).copied().ok_or_else(|| "the text ends before the value does".into())
    }

    fn take(&mut self, want: char) -> Result<(), String> {
        if self.peek()? != want {
            return Err(format!("expected `{want}` at character {}", self.at));
        }
        self.at += 1;
        Ok(())
    }

    fn word(&mut self, want: &str) -> bool {
        if self.c[self.at..].starts_with(&want.chars().collect::<Vec<char>>()[..]) {
            self.at += want.chars().count();
            return true;
        }
        false
    }

    fn string(&mut self) -> Result<String, String> {
        self.take('"')?;
        let mut s = String::new();
        loop {
            let c = *self.c.get(self.at).ok_or("a string is never closed")?;
            self.at += 1;
            match c {
                '"' => return Ok(s),
                '\\' => {
                    let e = *self.c.get(self.at).ok_or("an escape is never finished")?;
                    self.at += 1;
                    s.push(match e {
                        'n' => '\n',
                        'r' => '\r',
                        't' => '\t',
                        'b' => '\u{8}',
                        'f' => '\u{c}',
                        'u' => {
                            let hex: String = self.c.get(self.at..self.at + 4).unwrap_or(&[]).iter().collect();
                            let code = u32::from_str_radix(&hex, 16)
                                .map_err(|_| format!("`\\u{hex}` is not four hex digits"))?;
                            self.at += 4;
                            char::from_u32(code).ok_or(format!("`\\u{hex}` is not a character"))?
                        }
                        other => other,
                    });
                }
                c => s.push(c),
            }
        }
    }

    fn number(&mut self) -> Result<f64, String> {
        let start = self.at;
        while self.c.get(self.at).is_some_and(|c| "+-.eE0123456789".contains(*c)) {
            self.at += 1;
        }
        let text: String = self.c[start..self.at].iter().collect();
        text.parse().map_err(|_| format!("`{text}` is not a number"))
    }

    fn value(&mut self) -> Result<Json, String> {
        match self.peek()? {
            '{' => {
                self.at += 1;
                let mut kv = Vec::new();
                if self.peek()? == '}' {
                    self.at += 1;
                    return Ok(Json::Obj(kv));
                }
                loop {
                    let key = self.string()?;
                    self.take(':')?;
                    kv.push((key, self.value()?));
                    match self.peek()? {
                        ',' => self.at += 1,
                        _ => break,
                    }
                }
                self.take('}')?;
                Ok(Json::Obj(kv))
            }
            '[' => {
                self.at += 1;
                let mut items = Vec::new();
                if self.peek()? == ']' {
                    self.at += 1;
                    return Ok(Json::Arr(items));
                }
                loop {
                    items.push(self.value()?);
                    match self.peek()? {
                        ',' => self.at += 1,
                        _ => break,
                    }
                }
                self.take(']')?;
                Ok(Json::Arr(items))
            }
            '"' => Ok(Json::Str(self.string()?)),
            _ => {
                if self.word("true") {
                    Ok(Json::Bool(true))
                } else if self.word("false") {
                    Ok(Json::Bool(false))
                } else if self.word("null") {
                    Ok(Json::Null)
                } else {
                    Ok(Json::Num(self.number()?))
                }
            }
        }
    }
}

/// The shape this value would have as an array, when every leaf under it is a number.
fn shape(j: &Json) -> Option<Vec<usize>> {
    match j {
        Json::Null | Json::Bool(_) | Json::Num(_) => Some(Vec::new()),
        Json::Arr(items) => {
            let mut inner = if items.is_empty() { Some(Vec::new()) } else { shape(&items[0]) }?;
            if items.iter().any(|i| shape(i).as_ref() != Some(&inner)) {
                return None;
            }
            inner.insert(0, items.len());
            Some(inner)
        }
        _ => None,
    }
}

fn flatten(j: &Json, into: &mut Vec<u8>) {
    match j {
        Json::Null => into.extend_from_slice(&f32::NAN.to_le_bytes()),
        Json::Bool(b) => into.extend_from_slice(&(*b as u8 as f32).to_le_bytes()),
        Json::Num(v) => into.extend_from_slice(&(*v as f32).to_le_bytes()),
        Json::Arr(items) => items.iter().for_each(|i| flatten(i, into)),
        _ => {}
    }
}

fn frame(j: Json) -> Result<Data, String> {
    match j {
        Json::Str(s) => Ok(Data::string(s, Meta::new())),
        Json::Obj(kv) => {
            let mut map = IndexMap::new();
            for (k, v) in kv {
                map.insert(k, frame(v)?);
            }
            Ok(Data::table(map, Meta::new()))
        }
        other => match shape(&other) {
            Some(dims) => {
                let mut buf = Vec::new();
                flatten(&other, &mut buf);
                Data::array_f32(dims, buf, Meta::new()).map_err(|e| e.to_string())
            }
            // A list of strings, or a ragged one, is no array: it comes through keyed by position.
            None => {
                let Json::Arr(items) = other else { return Err("a value with no shape".into()) };
                let mut map = IndexMap::new();
                for (i, v) in items.into_iter().enumerate() {
                    map.insert(i.to_string(), frame(v)?);
                }
                Ok(Data::table(map, Meta::new()))
            }
        },
    }
}

impl Node for FromJson {
    fn process(
        &mut self,
        inp: &Inputs<'_>,
        out: &mut Outputs<'_>,
        _c: &mut NodeCtx,
        _p: &Params<'_>,
    ) -> NodeResult {
        let text = inp.get("input").ok_or("`input` is required")?.as_str()?;
        let c: Vec<char> = text.chars().collect();
        let at = c.iter().position(|c| *c == '{').ok_or("the text holds no object")?;
        let value = Reader { c, at }.value()?;
        out.set("out", frame(value)?);
        Ok(())
    }
}

static INPUTS: &[SlotDecl] = &[SlotDecl {
    name: "input",
    kind: SlotType::String,
    trigger_process: true,
    multi: false,
    required: true,
}];
static OUTPUTS: &[OutputDecl] = &[OutputDecl { name: "out", kind: SlotType::Table }];

static MANIFEST: Manifest = Manifest {
    tags: &[Tag::Transform, Tag::Text],
    doc: "The first object in a piece of text becomes a table.",
    inputs: INPUTS,
    outputs: OUTPUTS,
    params: &[],
    producer: false,
};

goofi_signal_sdk::export!(FromJson, MANIFEST);

← All nodes

This reference describes goofi 3.1.0(537cd394), generated from a running instance on 2026-09-06.