feat(json): add JSON Builder and JSON Fixer nodes

JSON Builder:
- Build JSON from up to 5 key-value pairs
- Auto-parse values (string or nested JSON)
- Category: 🚦ComfyUI_LLMs_Toolkit/JSON

JSON Fixer:
- Fix malformed JSON from LLM outputs
- Handle: trailing commas, single quotes, markdown code blocks
- Extract JSON from surrounding text
- Category: 🚦ComfyUI_LLMs_Toolkit/JSON

Also fixed JSON Extractor to use forceInput for upstream connection.
This commit is contained in:
科林_Cyril Pilgrim
2026-02-09 22:08:44 +08:00
parent 3f7a520085
commit fe293f74c5
3 changed files with 178 additions and 2 deletions
+66
View File
@@ -0,0 +1,66 @@
import json
import re
from typing import Tuple, Optional
class JSONBuilder:
"""
Build JSON object from multiple key-value inputs.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"key_1": ("STRING", {"default": ""}),
"value_1": ("STRING", {"default": "", "forceInput": True}),
"key_2": ("STRING", {"default": ""}),
"value_2": ("STRING", {"default": "", "forceInput": True}),
"key_3": ("STRING", {"default": ""}),
"value_3": ("STRING", {"default": "", "forceInput": True}),
"key_4": ("STRING", {"default": ""}),
"value_4": ("STRING", {"default": "", "forceInput": True}),
"key_5": ("STRING", {"default": ""}),
"value_5": ("STRING", {"default": "", "forceInput": True}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("json_string",)
FUNCTION = "build"
CATEGORY = "🚦ComfyUI_LLMs_Toolkit/JSON"
def build(
self,
key_1: str = "", value_1: str = "",
key_2: str = "", value_2: str = "",
key_3: str = "", value_3: str = "",
key_4: str = "", value_4: str = "",
key_5: str = "", value_5: str = ""
) -> Tuple[str]:
"""Build JSON from key-value pairs."""
result = {}
pairs = [
(key_1, value_1), (key_2, value_2), (key_3, value_3),
(key_4, value_4), (key_5, value_5)
]
for key, value in pairs:
if key and key.strip():
# Try to parse value as JSON, otherwise use as string
try:
parsed_value = json.loads(value)
result[key.strip()] = parsed_value
except (json.JSONDecodeError, TypeError):
result[key.strip()] = value
json_str = json.dumps(result, ensure_ascii=False, indent=2)
print(f"# [🚦 LLMs_Toolkit] built JSON with {len(result)} keys")
return (json_str,)
# Register the node
NODE_CLASS_MAPPINGS = {"JSONBuilder": JSONBuilder}
NODE_DISPLAY_NAME_MAPPINGS = {"JSONBuilder": "JSON Builder"}
+1 -2
View File
@@ -12,8 +12,7 @@ class JSONExtractor:
return {
"required": {
"json_string": ("STRING", {
"multiline": True,
"default": "{}"
"forceInput": True
}),
"key": ("STRING", {
"default": "",
+111
View File
@@ -0,0 +1,111 @@
import json
import re
from typing import Tuple
class JSONFixer:
"""
Fix malformed JSON from LLM outputs.
Common issues fixed:
- Trailing commas
- Single quotes instead of double quotes
- Unquoted keys
- Missing quotes around string values
- Markdown code block wrappers
- Extra text before/after JSON
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"forceInput": True})
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("fixed_json",)
FUNCTION = "fix"
CATEGORY = "🚦ComfyUI_LLMs_Toolkit/JSON"
def fix(self, text: str) -> Tuple[str]:
"""
Attempt to fix malformed JSON.
Args:
text: Raw text that should contain JSON
Returns:
Fixed JSON string, or error message if unfixable
"""
original = text
# Step 1: Try direct parse first (already valid)
try:
parsed = json.loads(text)
print(f"# [🚦 LLMs_Toolkit] JSON already valid")
return (json.dumps(parsed, ensure_ascii=False),)
except json.JSONDecodeError:
pass
# Step 2: Extract JSON from markdown code blocks
code_block_pattern = r"```(?:json)?\s*([\s\S]*?)```"
matches = re.findall(code_block_pattern, text)
if matches:
text = matches[0].strip()
# Step 3: Find JSON-like content (starts with { or [)
json_pattern = r'(\{[\s\S]*\}|\[[\s\S]*\])'
json_matches = re.findall(json_pattern, text)
if json_matches:
# Try each match
for match in json_matches:
text = match.strip()
try:
parsed = json.loads(text)
print(f"# [🚦 LLMs_Toolkit] extracted JSON from text")
return (json.dumps(parsed, ensure_ascii=False),)
except json.JSONDecodeError:
pass
# Step 4: Apply common fixes
fixed = text
# Remove trailing commas before } or ]
fixed = re.sub(r',\s*([}\]])', r'\1', fixed)
# Replace single quotes with double quotes (careful with apostrophes)
# Only replace quotes that look like JSON string delimiters
fixed = re.sub(r"'([^']*)':", r'"\1":', fixed) # Keys
fixed = re.sub(r":\s*'([^']*)'([,}\]])", r': "\1"\2', fixed) # Values
# Try parse after fixes
try:
parsed = json.loads(fixed)
print(f"# [🚦 LLMs_Toolkit] fixed JSON (trailing commas, quotes)")
return (json.dumps(parsed, ensure_ascii=False),)
except json.JSONDecodeError as e:
pass
# Step 5: Last resort - try to find any valid JSON substring
for i in range(len(original)):
if original[i] in '{[':
for j in range(len(original), i, -1):
try:
candidate = original[i:j]
parsed = json.loads(candidate)
print(f"# [🚦 LLMs_Toolkit] extracted valid JSON substring")
return (json.dumps(parsed, ensure_ascii=False),)
except json.JSONDecodeError:
continue
# Failed to fix
error_msg = f"Unable to fix JSON: {str(e)}"
print(f"# [🚦 LLMs_Toolkit] ✗ {error_msg}")
return (error_msg,)
# Register the node
NODE_CLASS_MAPPINGS = {"JSONFixer": JSONFixer}
NODE_DISPLAY_NAME_MAPPINGS = {"JSONFixer": "JSON Fixer"}