From 1fdbdbd094c78b239dd2795a4f34cbff6951adc1 Mon Sep 17 00:00:00 2001 From: Mubashir Rahim Date: Wed, 3 Jun 2026 14:34:22 +0500 Subject: [PATCH] fix(import): preserve apostrophes in valid JSON imports parse_json unconditionally replaced every single quote with a double quote (`.replace("'", '"')`) before calling json.loads. This corrupted any valid JSON string value containing an apostrophe, e.g. a person's surname "Sarah O'Brien" became "Sarah O"Brien", causing the parse to fail with "Invalid JSON" and the whole import to be rejected. Parse strict JSON first so well-formed payloads (including apostrophes in string values) import correctly, and only fall back to the lenient single-quote replacement for Python-dict-style payloads that are not valid JSON on their own. Adds regression tests covering an apostrophe-bearing JSON value and the retained single-quoted fallback. Co-Authored-By: Claude Opus 4.8 --- .../flowsint_core/imports/json/parse_json.py | 11 +++++- .../tests/import/json/test_json_import.py | 37 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/flowsint-core/src/flowsint_core/imports/json/parse_json.py b/flowsint-core/src/flowsint_core/imports/json/parse_json.py index 98cdedd..cecf62f 100644 --- a/flowsint-core/src/flowsint_core/imports/json/parse_json.py +++ b/flowsint-core/src/flowsint_core/imports/json/parse_json.py @@ -23,8 +23,15 @@ def parse_json( try: file_bytes = file_bytes.lstrip() entities: Dict[str, Entity] = {} - my_bytes_value = file_bytes.decode().replace("'", '"') - graph = json.loads(my_bytes_value) + decoded = file_bytes.decode() + # Parse strict JSON first so apostrophes inside string values (e.g. + # "Sarah O'Brien") are preserved. Only fall back to the lenient + # single-quote replacement for Python-dict-style payloads that are not + # valid JSON on their own. + try: + graph = json.loads(decoded) + except json.JSONDecodeError: + graph = json.loads(decoded.replace("'", '"')) node_key = next((k for k in VALID_NODES_KEYS if k in graph), None) edge_key = next((k for k in VALID_EDGES_KEYS if k in graph), None) if node_key is None: diff --git a/flowsint-core/tests/import/json/test_json_import.py b/flowsint-core/tests/import/json/test_json_import.py index a1930e0..fded716 100644 --- a/flowsint-core/tests/import/json/test_json_import.py +++ b/flowsint-core/tests/import/json/test_json_import.py @@ -65,3 +65,40 @@ def test_standard_json_import_without_label(): """Test basic standard JSON import.""" results = parse_json(standard_json_without_label, max_preview_rows=100) assert "Username" in results.entities + + +# Valid JSON whose string values contain apostrophes (very common in OSINT +# data, e.g. surnames like "O'Brien"). This is well-formed JSON and must import. +json_with_apostrophe = b"""{ + "nodes": [ + {"id": "1", "label": "Sarah O'Brien", "type": "individual"}, + {"id": "2", "label": "Bob", "type": "individual"} + ], + "edges": [ + {"source": "1", "target": "2", "label": "KNOWS"} + ] +}""" + + +def test_json_import_preserves_apostrophes_in_string_values(): + """Valid JSON with an apostrophe in a value must parse and keep the value. + + Regression: the parser used to blindly replace every single quote with a + double quote, which corrupted valid JSON (turning "Sarah O'Brien" into + "Sarah O"Brien") and raised "Invalid JSON". + """ + results = parse_json(json_with_apostrophe, max_preview_rows=100) + + assert "Individual" in results.entities + labels = { + str(preview.obj.nodeLabel) + for preview in results.entities["Individual"].results + } + assert "Sarah O'Brien" in labels + + +def test_json_import_still_accepts_single_quoted_payload(): + """Python-dict-style payloads (single-quoted) remain supported as fallback.""" + single_quoted = b"{'nodes': [{'id': '1', 'label': 'Alice', 'type': 'individual'}], 'edges': []}" + results = parse_json(single_quoted, max_preview_rows=100) + assert "Individual" in results.entities