import json import re from typing import Any, Dict, List, Set, Union class PruneOptions: def __init__( self, remove_nulls: bool = True, remove_empty_strings: bool = True, remove_empty_collections: bool = True, trim_strings: bool = False, # ⚠️ Keep False for clinical text compact_lists: bool = True, preserve_paths: Set[str] = None, output_minified_string: bool = True, ): self.remove_nulls = remove_nulls self.remove_empty_strings = remove_empty_strings self.remove_empty_collections = remove_empty_collections self.trim_strings = trim_strings self.compact_lists = compact_lists self.preserve_paths = preserve_paths or set() self.output_minified_string = output_minified_string def _minify_lossless(text: str) -> str: """Drop whitespace outside strings, keep everything inside intact.""" out = [] in_string = False escaping = False for ch in text: if in_string: out.append(ch) if escaping: escaping = False elif ch == "\\": escaping = True elif ch == '"': in_string = False else: if ch == '"': in_string = True out.append(ch) elif ch in " \t\n\r": continue # drop whitespace outside strings else: out.append(ch) return "".join(out) def _preserve(path: str, options: PruneOptions) -> bool: return path and path in options.preserve_paths def _prune(value: Any, options: PruneOptions, path: str) -> Any: if _preserve(path, options): return value if value is None: return None if options.remove_nulls else value if isinstance(value, (bool, int, float)): return value if isinstance(value, str): if options.trim_strings: t = value.strip() if options.remove_empty_strings and t == "": return None return t else: if options.remove_empty_strings and value == "": return None return value if isinstance(value, list): out = [] for i, item in enumerate(value): pruned = _prune(item, options, f"{path}[{i}]") if options.compact_lists: if pruned is None: continue if isinstance(pruned, (list, dict)) and not pruned and options.remove_empty_collections: continue out.append(pruned) if options.remove_empty_collections and not out: return None return out if isinstance(value, dict): out = {} for k, v in value.items(): next_path = f"{path}.{k}" if path else k pruned = _prune(v, options, next_path) if pruned is None and options.remove_nulls: continue if isinstance(pruned, str) and options.remove_empty_strings and pruned == "": continue if isinstance(pruned, (list, dict)) and not pruned and options.remove_empty_collections: continue out[k] = pruned if options.remove_empty_collections and not out: return None return out return value def slim_api_json(response_body: str, options: PruneOptions = PruneOptions()) -> Union[str, dict]: # Pure minify if pruning disabled if not any([options.remove_nulls, options.remove_empty_strings, options.remove_empty_collections, options.compact_lists, options.trim_strings]): s = _minify_lossless(response_body) return s if options.output_minified_string else json.loads(s) decoded = json.loads(response_body) pruned = _prune(decoded, options, path="") return json.dumps(pruned, separators=(",", ":")) if options.output_minified_string else pruned