Add Dumas JSON utility batch

This commit is contained in:
2026-07-21 11:03:17 +00:00
parent 24d8ac5e14
commit 4ad9e93b63
3 changed files with 366 additions and 0 deletions

View File

@@ -24,6 +24,21 @@
- Output: updated `JSON` - Output: updated `JSON`
- Writes a parsed JSON value into a nested path without mutating the original object. - Writes a parsed JSON value into a nested path without mutating the original object.
- `Dumas JSON Has Key`
- Inputs: `json_object`, `path`
- Output: `BOOLEAN`
- Checks whether a nested path exists.
- `Dumas JSON Remove Key`
- Inputs: `json_object`, `path`
- Output: updated `JSON`
- Removes a nested key or array item by path.
- `Dumas JSON Pick Fields`
- Inputs: `json_object`, `paths`
- Output: filtered `JSON`
- Builds a new object from newline-separated paths that exist in the input.
- `Dumas JSON Merge Objects` - `Dumas JSON Merge Objects`
- Inputs: `base_object`, `overlay_object` - Inputs: `base_object`, `overlay_object`
- Output: merged `JSON` - Output: merged `JSON`
@@ -39,6 +54,16 @@
- Output: `INT` - Output: `INT`
- Returns the length of a JSON array. - Returns the length of a JSON array.
- `Dumas JSON Array Append`
- Inputs: `json_array`, `value_json`
- Output: updated `JSON`
- Appends a parsed JSON value to the end of an array.
- `Dumas JSON Array Slice`
- Inputs: `json_array`, `start`, `end`, `step`
- Output: sliced `JSON`
- Returns an array slice. `end=0` means "slice to the end".
- `Dumas JSON Array Iterator` - `Dumas JSON Array Iterator`
- Inputs: `json_input`, `index`, `mode` - Inputs: `json_input`, `index`, `mode`
- Outputs: `item`, `current_index`, `total_items` - Outputs: `item`, `current_index`, `total_items`
@@ -49,6 +74,16 @@
- Outputs: `key`, `value`, `current_index`, `total_items` - Outputs: `key`, `value`, `current_index`, `total_items`
- Returns the current object entry in insertion order, with `mode` set to `fixed`, `incr`, or `decr`. - Returns the current object entry in insertion order, with `mode` set to `fixed`, `incr`, or `decr`.
- `Dumas JSON Flatten`
- Input: `json_input`
- Output: flat `JSON`
- Flattens nested objects and arrays into dot-path keys.
- `Dumas JSON Unflatten`
- Input: `flat_json_object`
- Output: nested `JSON`
- Rebuilds nested JSON from a flat dot-path object.
All nodes live in the `Dumas/JSON` category. All nodes live in the `Dumas/JSON` category.
## Installation ## Installation
@@ -87,6 +122,16 @@ settings.render.width
{"enabled": true, "retries": 3} {"enabled": true, "retries": 3}
``` ```
`Dumas JSON Pick Fields` expects one path per line:
```text
user.name
meta.active
items.0.id
```
`Dumas JSON Array Slice` treats `end=0` as "use the rest of the array".
`Dumas JSON Array Iterator` clamps to valid bounds: `Dumas JSON Array Iterator` clamps to valid bounds:
```text ```text
@@ -97,6 +142,8 @@ decr -> use index - 1
`Dumas JSON Object Iterator` uses the same index rules and iterates object entries in their existing key order. `Dumas JSON Object Iterator` uses the same index rules and iterates object entries in their existing key order.
`Dumas JSON Flatten` and `Dumas JSON Unflatten` use the same dot-path format as the other path-based nodes.
## Roadmap ## 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. This repo is intended to grow into a broader set of Dumas-branded generic utility nodes, including JSON helpers and adjacent data-manipulation tools.

View File

@@ -43,6 +43,21 @@ def _get_json_path_value(data, path):
return current return current
def _has_json_path(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 False
current = current[segment]
continue
if not isinstance(current, dict) or segment not in current:
return False
current = current[segment]
return True
def _build_missing_container(next_segment): def _build_missing_container(next_segment):
return [] if isinstance(next_segment, int) else {} return [] if isinstance(next_segment, int) else {}
@@ -104,6 +119,50 @@ def _set_json_path_value(data, path, value):
return _set_json_path_value_recursive(data, segments, value) return _set_json_path_value_recursive(data, segments, value)
def _remove_json_path_value_recursive(current, segments):
if not segments:
return _clone_container(current)
segment = segments[0]
is_last = len(segments) == 1
if isinstance(segment, int):
if not isinstance(current, list) or segment >= len(current):
return None
result = list(current)
if is_last:
del result[segment]
return result
updated = _remove_json_path_value_recursive(result[segment], segments[1:])
if updated is None:
return None
result[segment] = updated
return result
if not isinstance(current, dict) or segment not in current:
return None
result = dict(current)
if is_last:
del result[segment]
return result
updated = _remove_json_path_value_recursive(result[segment], segments[1:])
if updated is None:
return None
result[segment] = updated
return result
def _remove_json_path_value(data, path):
segments = _parse_json_path(path)
if not segments:
return _clone_container(data)
return _remove_json_path_value_recursive(data, segments)
def _merge_json_objects(base_object, overlay_object): def _merge_json_objects(base_object, overlay_object):
if not isinstance(base_object, dict) or not isinstance(overlay_object, dict): if not isinstance(base_object, dict) or not isinstance(overlay_object, dict):
return None return None
@@ -118,6 +177,46 @@ def _merge_json_objects(base_object, overlay_object):
return merged return merged
def _flatten_json_value(value, prefix="", result=None):
if result is None:
result = {}
if isinstance(value, dict):
if not value and prefix:
result[prefix] = {}
return result
for key, nested_value in value.items():
next_prefix = f"{prefix}.{key}" if prefix else str(key)
_flatten_json_value(nested_value, next_prefix, result)
return result
if isinstance(value, list):
if not value and prefix:
result[prefix] = []
return result
for index, nested_value in enumerate(value):
next_prefix = f"{prefix}.{index}" if prefix else str(index)
_flatten_json_value(nested_value, next_prefix, result)
return result
result[prefix] = value
return result
def _unflatten_json_object(flat_object):
if not isinstance(flat_object, dict):
return None
result = {}
for path, value in flat_object.items():
if not isinstance(path, str):
return None
result = _set_json_path_value(result, path, value)
if result is None:
return None
return result
def _resolve_iteration_index(index, mode, total_items): def _resolve_iteration_index(index, mode, total_items):
if total_items <= 0: if total_items <= 0:
return -1 return -1
@@ -216,6 +315,69 @@ class DumasJSONSetValueNode:
return (_set_json_path_value(json_object, path, value),) return (_set_json_path_value(json_object, path, value),)
class DumasJSONHasKeyNode:
CATEGORY = "Dumas/JSON"
RETURN_TYPES = ("BOOLEAN",)
FUNCTION = "has_key"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"json_object": ("JSON",),
"path": ("STRING", {"default": "", "multiline": False}),
}
}
def has_key(self, json_object, path):
return (_has_json_path(json_object, path),)
class DumasJSONRemoveKeyNode:
CATEGORY = "Dumas/JSON"
RETURN_TYPES = ("JSON",)
FUNCTION = "remove_key"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"json_object": ("JSON",),
"path": ("STRING", {"default": "", "multiline": False}),
}
}
def remove_key(self, json_object, path):
return (_remove_json_path_value(json_object, path),)
class DumasJSONPickFieldsNode:
CATEGORY = "Dumas/JSON"
RETURN_TYPES = ("JSON",)
FUNCTION = "pick_fields"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"json_object": ("JSON",),
"paths": ("STRING", {"multiline": True, "default": ""}),
}
}
def pick_fields(self, json_object, paths):
result = {}
for raw_path in paths.splitlines():
path = raw_path.strip()
if not path or not _has_json_path(json_object, path):
continue
value = _get_json_path_value(json_object, path)
result = _set_json_path_value(result, path, value)
if result is None:
return (None,)
return (result,)
class DumasJSONMergeObjectsNode: class DumasJSONMergeObjectsNode:
CATEGORY = "Dumas/JSON" CATEGORY = "Dumas/JSON"
RETURN_TYPES = ("JSON",) RETURN_TYPES = ("JSON",)
@@ -274,6 +436,56 @@ class DumasJSONArrayLengthNode:
return (len(json_array),) return (len(json_array),)
class DumasJSONArrayAppendNode:
CATEGORY = "Dumas/JSON"
RETURN_TYPES = ("JSON",)
FUNCTION = "append_item"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"json_array": ("JSON",),
"value_json": ("STRING", {"multiline": True, "default": "null"}),
}
}
def append_item(self, json_array, value_json):
if not isinstance(json_array, list):
return (None,)
try:
value = json.loads(value_json)
except json.JSONDecodeError as exc:
print(f"[DumasNodes] Error decoding JSON value: {exc}")
return (None,)
result = list(json_array)
result.append(value)
return (result,)
class DumasJSONArraySliceNode:
CATEGORY = "Dumas/JSON"
RETURN_TYPES = ("JSON",)
FUNCTION = "slice_array"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"json_array": ("JSON",),
"start": ("INT", {"default": 0, "step": 1}),
"end": ("INT", {"default": 0, "step": 1}),
"step": ("INT", {"default": 1, "step": 1, "min": 1}),
}
}
def slice_array(self, json_array, start, end, step):
if not isinstance(json_array, list):
return (None,)
actual_end = None if end == 0 else end
return (json_array[start:actual_end:step],)
class DumasJSONArrayIteratorNode: class DumasJSONArrayIteratorNode:
CATEGORY = "Dumas/JSON" CATEGORY = "Dumas/JSON"
RETURN_TYPES = ("JSON", "INT", "INT") RETURN_TYPES = ("JSON", "INT", "INT")
@@ -331,16 +543,57 @@ class DumasJSONObjectIteratorNode:
return (key, value, current_index, total_items) return (key, value, current_index, total_items)
class DumasJSONFlattenNode:
CATEGORY = "Dumas/JSON"
RETURN_TYPES = ("JSON",)
FUNCTION = "flatten_json"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"json_input": ("JSON",),
}
}
def flatten_json(self, json_input):
return (_flatten_json_value(json_input),)
class DumasJSONUnflattenNode:
CATEGORY = "Dumas/JSON"
RETURN_TYPES = ("JSON",)
FUNCTION = "unflatten_json"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"flat_json_object": ("JSON",),
}
}
def unflatten_json(self, flat_json_object):
return (_unflatten_json_object(flat_json_object),)
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
"DumasJSONStringToObject": DumasJSONStringToObjectNode, "DumasJSONStringToObject": DumasJSONStringToObjectNode,
"DumasJSONObjectToString": DumasJSONObjectToStringNode, "DumasJSONObjectToString": DumasJSONObjectToStringNode,
"DumasJSONGetValue": DumasJSONGetValueNode, "DumasJSONGetValue": DumasJSONGetValueNode,
"DumasJSONSetValue": DumasJSONSetValueNode, "DumasJSONSetValue": DumasJSONSetValueNode,
"DumasJSONHasKey": DumasJSONHasKeyNode,
"DumasJSONRemoveKey": DumasJSONRemoveKeyNode,
"DumasJSONPickFields": DumasJSONPickFieldsNode,
"DumasJSONMergeObjects": DumasJSONMergeObjectsNode, "DumasJSONMergeObjects": DumasJSONMergeObjectsNode,
"DumasJSONKeys": DumasJSONKeysNode, "DumasJSONKeys": DumasJSONKeysNode,
"DumasJSONArrayLength": DumasJSONArrayLengthNode, "DumasJSONArrayLength": DumasJSONArrayLengthNode,
"DumasJSONArrayAppend": DumasJSONArrayAppendNode,
"DumasJSONArraySlice": DumasJSONArraySliceNode,
"DumasJSONArrayIterator": DumasJSONArrayIteratorNode, "DumasJSONArrayIterator": DumasJSONArrayIteratorNode,
"DumasJSONObjectIterator": DumasJSONObjectIteratorNode, "DumasJSONObjectIterator": DumasJSONObjectIteratorNode,
"DumasJSONFlatten": DumasJSONFlattenNode,
"DumasJSONUnflatten": DumasJSONUnflattenNode,
} }
NODE_DISPLAY_NAME_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = {
@@ -348,9 +601,16 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"DumasJSONObjectToString": "Dumas JSON Object to String", "DumasJSONObjectToString": "Dumas JSON Object to String",
"DumasJSONGetValue": "Dumas JSON Get Value", "DumasJSONGetValue": "Dumas JSON Get Value",
"DumasJSONSetValue": "Dumas JSON Set Value", "DumasJSONSetValue": "Dumas JSON Set Value",
"DumasJSONHasKey": "Dumas JSON Has Key",
"DumasJSONRemoveKey": "Dumas JSON Remove Key",
"DumasJSONPickFields": "Dumas JSON Pick Fields",
"DumasJSONMergeObjects": "Dumas JSON Merge Objects", "DumasJSONMergeObjects": "Dumas JSON Merge Objects",
"DumasJSONKeys": "Dumas JSON Keys", "DumasJSONKeys": "Dumas JSON Keys",
"DumasJSONArrayLength": "Dumas JSON Array Length", "DumasJSONArrayLength": "Dumas JSON Array Length",
"DumasJSONArrayAppend": "Dumas JSON Array Append",
"DumasJSONArraySlice": "Dumas JSON Array Slice",
"DumasJSONArrayIterator": "Dumas JSON Array Iterator", "DumasJSONArrayIterator": "Dumas JSON Array Iterator",
"DumasJSONObjectIterator": "Dumas JSON Object Iterator", "DumasJSONObjectIterator": "Dumas JSON Object Iterator",
"DumasJSONFlatten": "Dumas JSON Flatten",
"DumasJSONUnflatten": "Dumas JSON Unflatten",
} }

View File

@@ -1,11 +1,18 @@
import unittest import unittest
from dumas_json_nodes import ( from dumas_json_nodes import (
DumasJSONArrayAppendNode,
DumasJSONArrayLengthNode, DumasJSONArrayLengthNode,
DumasJSONArrayIteratorNode, DumasJSONArrayIteratorNode,
DumasJSONArraySliceNode,
DumasJSONFlattenNode,
DumasJSONGetValueNode, DumasJSONGetValueNode,
DumasJSONHasKeyNode,
DumasJSONKeysNode, DumasJSONKeysNode,
DumasJSONMergeObjectsNode, DumasJSONMergeObjectsNode,
DumasJSONPickFieldsNode,
DumasJSONRemoveKeyNode,
DumasJSONUnflattenNode,
DumasJSONObjectIteratorNode, DumasJSONObjectIteratorNode,
DumasJSONObjectToStringNode, DumasJSONObjectToStringNode,
DumasJSONStringToObjectNode, DumasJSONStringToObjectNode,
@@ -48,6 +55,30 @@ class DumasJSONNodeTests(unittest.TestCase):
self.assertEqual(result, ({"user": {"name": "Dumas"}},)) self.assertEqual(result, ({"user": {"name": "Dumas"}},))
def test_has_key_checks_nested_paths(self):
node = DumasJSONHasKeyNode()
result = node.has_key({"user": {"name": "Chris"}}, "user.name")
self.assertEqual(result, (True,))
def test_remove_key_removes_nested_values(self):
node = DumasJSONRemoveKeyNode()
result = node.remove_key({"user": {"name": "Chris", "role": "owner"}}, "user.role")
self.assertEqual(result, ({"user": {"name": "Chris"}},))
def test_pick_fields_builds_filtered_object(self):
node = DumasJSONPickFieldsNode()
result = node.pick_fields(
{"user": {"name": "Chris", "role": "owner"}, "meta": {"active": True}},
"user.name\nmeta.active",
)
self.assertEqual(result, ({"user": {"name": "Chris"}, "meta": {"active": True}},))
def test_merge_objects_deep_merges_dictionaries(self): def test_merge_objects_deep_merges_dictionaries(self):
node = DumasJSONMergeObjectsNode() node = DumasJSONMergeObjectsNode()
@@ -75,6 +106,20 @@ class DumasJSONNodeTests(unittest.TestCase):
self.assertEqual(result, (4,)) self.assertEqual(result, (4,))
def test_array_append_appends_parsed_value(self):
node = DumasJSONArrayAppendNode()
result = node.append_item([1, 2], '{"name":"Dumas"}')
self.assertEqual(result, ([1, 2, {"name": "Dumas"}],))
def test_array_slice_returns_subarray(self):
node = DumasJSONArraySliceNode()
result = node.slice_array([0, 1, 2, 3, 4], 1, 4, 2)
self.assertEqual(result, ([1, 3],))
def test_array_iterator_fixed_mode_uses_given_index(self): def test_array_iterator_fixed_mode_uses_given_index(self):
node = DumasJSONArrayIteratorNode() node = DumasJSONArrayIteratorNode()
@@ -145,6 +190,20 @@ class DumasJSONNodeTests(unittest.TestCase):
self.assertEqual(result, (None, None, -1, 0)) self.assertEqual(result, (None, None, -1, 0))
def test_flatten_converts_nested_data_to_path_map(self):
node = DumasJSONFlattenNode()
result = node.flatten_json({"user": {"name": "Chris"}, "items": [{"id": 1}]})
self.assertEqual(result, ({"user.name": "Chris", "items.0.id": 1},))
def test_unflatten_rebuilds_nested_data(self):
node = DumasJSONUnflattenNode()
result = node.unflatten_json({"user.name": "Chris", "items.0.id": 1})
self.assertEqual(result, ({"user": {"name": "Chris"}, "items": [{"id": 1}]},))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()