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 <noreply@anthropic.com>
This commit is contained in:
Mubashir Rahim
2026-06-03 14:34:22 +05:00
parent 0b60c81361
commit 1fdbdbd094
2 changed files with 46 additions and 2 deletions

View File

@@ -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:

View File

@@ -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