From 3b918fd36f21bbbeefde418e33bc710cba0b5a3c Mon Sep 17 00:00:00 2001 From: Chris Dumas Date: Tue, 21 Jul 2026 10:48:10 +0000 Subject: [PATCH] Expand Dumas JSON utility nodes --- README.md | 61 +++++++-- dumas_json_nodes.py | 219 +++++++++++++++++++++++++++++++++ tests/test_dumas_json_nodes.py | 61 ++++++++- 3 files changed, 330 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 868a6e9..3475ff0 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,45 @@ # DumasNodes -`DumasNodes` is a ComfyUI custom-node pack for generic utility workflows under the Dumas brand. The repo starts by replicating the JSON string parsing helper from [`a-und-b/ComfyUI_JSON_Helper`](https://github.com/a-und-b/ComfyUI_JSON_Helper) and rebadging it into a reusable `Dumas` namespace. +`DumasNodes` is a ComfyUI custom-node pack for generic utility workflows under the Dumas brand. ## Included Nodes - `Dumas JSON String to Object` - - Converts a JSON string into a JSON object so downstream nodes can consume structured data. - - Lives in the `Dumas/JSON` category. + - Input: `json_string` + - Output: parsed `JSON` + - Use it to turn raw JSON text into a structured object. + +- `Dumas JSON Object to String` + - Inputs: `json_object`, `pretty`, `sort_keys` + - Output: serialized `STRING` + - Use it to compact or pretty-print JSON for prompts, logs, or file output. + +- `Dumas JSON Get Value` + - Inputs: `json_object`, `path` + - Output: `JSON` + - Reads a nested value using dot paths like `user.profile.name` or `items.0.id`. + +- `Dumas JSON Set Value` + - Inputs: `json_object`, `path`, `value_json` + - Output: updated `JSON` + - Writes a parsed JSON value into a nested path without mutating the original object. + +- `Dumas JSON Merge Objects` + - Inputs: `base_object`, `overlay_object` + - Output: merged `JSON` + - Deep-merges dictionaries so overlay values replace or extend the base object. + +- `Dumas JSON Keys` + - Input: `json_object` + - Outputs: `keys` as JSON array, `count` as integer + - Lists the top-level keys of an object and reports how many there are. + +- `Dumas JSON Array Length` + - Input: `json_array` + - Output: `INT` + - Returns the length of a JSON array. + +All nodes live in the `Dumas/JSON` category. ## Installation @@ -26,16 +59,28 @@ ## Usage -Add `Dumas JSON String to Object` anywhere you need to bridge plain-text JSON output into structured JSON input. +Use dot paths for nested access: + +```text +user.profile.name +items.0.id +settings.render.width +``` + +`Dumas JSON Set Value` expects `value_json` to be valid JSON, so strings should be quoted: + +```json +"Dumas" +``` + +```json +{"enabled": true, "retries": 3} +``` ## Roadmap This repo is intended to grow into a broader set of Dumas-branded generic utility nodes, including JSON helpers and adjacent data-manipulation tools. -## Credits - -The first node is derived from the upstream `ComfyUI_JSON_Helper` project by `a-und-b`, then repackaged and branded for the Dumas node collection. - ## License MIT. diff --git a/dumas_json_nodes.py b/dumas_json_nodes.py index 9025149..1f17bd6 100644 --- a/dumas_json_nodes.py +++ b/dumas_json_nodes.py @@ -1,6 +1,91 @@ +import copy import json +def _parse_json_path(path): + if not path.strip(): + return [] + + segments = [] + for raw_segment in path.split("."): + segment = raw_segment.strip() + if not segment: + continue + if segment.isdigit(): + segments.append(int(segment)) + else: + segments.append(segment) + return segments + + +def _get_json_path_value(data, path): + current = data + for segment in _parse_json_path(path): + if isinstance(segment, int): + if not isinstance(current, list) or segment >= len(current): + return None + current = current[segment] + continue + + if not isinstance(current, dict) or segment not in current: + return None + current = current[segment] + return current + + +def _set_json_path_value(data, path, value): + if not isinstance(data, dict): + return None + + segments = _parse_json_path(path) + if not segments: + return value + + result = copy.deepcopy(data) + current = result + + for index, segment in enumerate(segments[:-1]): + next_segment = segments[index + 1] + if isinstance(segment, int): + if not isinstance(current, list): + return None + while segment >= len(current): + current.append({} if not isinstance(next_segment, int) else []) + current = current[segment] + continue + + if segment not in current or not isinstance(current[segment], (dict, list)): + current[segment] = [] if isinstance(next_segment, int) else {} + current = current[segment] + + last_segment = segments[-1] + if isinstance(last_segment, int): + if not isinstance(current, list): + return None + while last_segment >= len(current): + current.append(None) + current[last_segment] = value + return result + + if not isinstance(current, dict): + return None + current[last_segment] = value + return result + + +def _merge_json_objects(base_object, overlay_object): + if not isinstance(base_object, dict) or not isinstance(overlay_object, dict): + return None + + merged = copy.deepcopy(base_object) + for key, value in overlay_object.items(): + if isinstance(merged.get(key), dict) and isinstance(value, dict): + merged[key] = _merge_json_objects(merged[key], value) + else: + merged[key] = copy.deepcopy(value) + return merged + + class DumasJSONStringToObjectNode: CATEGORY = "Dumas/JSON" RETURN_TYPES = ("JSON",) @@ -23,10 +108,144 @@ class DumasJSONStringToObjectNode: return (None,) +class DumasJSONObjectToStringNode: + CATEGORY = "Dumas/JSON" + RETURN_TYPES = ("STRING",) + FUNCTION = "convert_object_to_string" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "json_object": ("JSON",), + "pretty": ("BOOLEAN", {"default": True}), + "sort_keys": ("BOOLEAN", {"default": False}), + } + } + + def convert_object_to_string(self, json_object, pretty, sort_keys): + indent = 2 if pretty else None + separators = None if pretty else (",", ":") + return (json.dumps(json_object, indent=indent, sort_keys=sort_keys, separators=separators),) + + +class DumasJSONGetValueNode: + CATEGORY = "Dumas/JSON" + RETURN_TYPES = ("JSON",) + FUNCTION = "get_value" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "json_object": ("JSON",), + "path": ("STRING", {"default": "", "multiline": False}), + } + } + + def get_value(self, json_object, path): + return (_get_json_path_value(json_object, path),) + + +class DumasJSONSetValueNode: + CATEGORY = "Dumas/JSON" + RETURN_TYPES = ("JSON",) + FUNCTION = "set_value" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "json_object": ("JSON",), + "path": ("STRING", {"default": "", "multiline": False}), + "value_json": ("STRING", {"multiline": True, "default": "null"}), + } + } + + def set_value(self, json_object, path, value_json): + try: + value = json.loads(value_json) + except json.JSONDecodeError as exc: + print(f"[DumasNodes] Error decoding JSON value: {exc}") + return (None,) + + return (_set_json_path_value(json_object, path, value),) + + +class DumasJSONMergeObjectsNode: + CATEGORY = "Dumas/JSON" + RETURN_TYPES = ("JSON",) + FUNCTION = "merge_objects" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "base_object": ("JSON",), + "overlay_object": ("JSON",), + } + } + + def merge_objects(self, base_object, overlay_object): + return (_merge_json_objects(base_object, overlay_object),) + + +class DumasJSONKeysNode: + CATEGORY = "Dumas/JSON" + RETURN_TYPES = ("JSON", "INT") + RETURN_NAMES = ("keys", "count") + FUNCTION = "list_keys" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "json_object": ("JSON",), + } + } + + def list_keys(self, json_object): + if not isinstance(json_object, dict): + return (None, 0) + keys = list(json_object.keys()) + return (keys, len(keys)) + + +class DumasJSONArrayLengthNode: + CATEGORY = "Dumas/JSON" + RETURN_TYPES = ("INT",) + FUNCTION = "get_length" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "json_array": ("JSON",), + } + } + + def get_length(self, json_array): + if not isinstance(json_array, list): + return (0,) + return (len(json_array),) + + NODE_CLASS_MAPPINGS = { "DumasJSONStringToObject": DumasJSONStringToObjectNode, + "DumasJSONObjectToString": DumasJSONObjectToStringNode, + "DumasJSONGetValue": DumasJSONGetValueNode, + "DumasJSONSetValue": DumasJSONSetValueNode, + "DumasJSONMergeObjects": DumasJSONMergeObjectsNode, + "DumasJSONKeys": DumasJSONKeysNode, + "DumasJSONArrayLength": DumasJSONArrayLengthNode, } NODE_DISPLAY_NAME_MAPPINGS = { "DumasJSONStringToObject": "Dumas JSON String to Object", + "DumasJSONObjectToString": "Dumas JSON Object to String", + "DumasJSONGetValue": "Dumas JSON Get Value", + "DumasJSONSetValue": "Dumas JSON Set Value", + "DumasJSONMergeObjects": "Dumas JSON Merge Objects", + "DumasJSONKeys": "Dumas JSON Keys", + "DumasJSONArrayLength": "Dumas JSON Array Length", } diff --git a/tests/test_dumas_json_nodes.py b/tests/test_dumas_json_nodes.py index be9222c..02e0190 100644 --- a/tests/test_dumas_json_nodes.py +++ b/tests/test_dumas_json_nodes.py @@ -1,9 +1,16 @@ import unittest -from dumas_json_nodes import DumasJSONStringToObjectNode +from dumas_json_nodes import ( + DumasJSONArrayLengthNode, + DumasJSONGetValueNode, + DumasJSONKeysNode, + DumasJSONMergeObjectsNode, + DumasJSONObjectToStringNode, + DumasJSONStringToObjectNode, + DumasJSONSetValueNode, +) - -class DumasJSONStringToObjectNodeTests(unittest.TestCase): +class DumasJSONNodeTests(unittest.TestCase): def test_parses_valid_json(self): node = DumasJSONStringToObjectNode() @@ -18,6 +25,54 @@ class DumasJSONStringToObjectNodeTests(unittest.TestCase): self.assertEqual(result, (None,)) + def test_object_to_string_can_pretty_print(self): + node = DumasJSONObjectToStringNode() + + result = node.convert_object_to_string({"b": 1, "a": 2}, True, True) + + self.assertEqual(result, ('{\n "a": 2,\n "b": 1\n}',)) + + def test_get_value_reads_nested_paths(self): + node = DumasJSONGetValueNode() + + result = node.get_value({"items": [{"id": "a1"}]}, "items.0.id") + + self.assertEqual(result, ("a1",)) + + def test_set_value_writes_nested_paths(self): + node = DumasJSONSetValueNode() + + result = node.set_value({"user": {"name": "Chris"}}, "user.name", '"Dumas"') + + self.assertEqual(result, ({"user": {"name": "Dumas"}},)) + + def test_merge_objects_deep_merges_dictionaries(self): + node = DumasJSONMergeObjectsNode() + + result = node.merge_objects( + {"user": {"name": "Chris", "flags": {"admin": False}}}, + {"user": {"flags": {"admin": True}, "role": "owner"}}, + ) + + self.assertEqual( + result, + ({"user": {"name": "Chris", "flags": {"admin": True}, "role": "owner"}},), + ) + + def test_keys_returns_key_list_and_count(self): + node = DumasJSONKeysNode() + + result = node.list_keys({"name": "Dumas", "kind": "nodes"}) + + self.assertEqual(result, (["name", "kind"], 2)) + + def test_array_length_returns_length(self): + node = DumasJSONArrayLengthNode() + + result = node.get_length([1, 2, 3, 4]) + + self.assertEqual(result, (4,)) + if __name__ == "__main__": unittest.main()