From e55d0e6c75437af79f3338aca1ec12c7ea16e3bd Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Sat, 18 Jul 2026 20:54:50 +0800 Subject: [PATCH] fix(dataprep): skip .jsonl lines that are valid JSON but not objects (#7195) * fix(dataprep): skip .jsonl lines that are valid JSON but not objects `_read_file_by_format` json.loads each line and hands the result to `_extract_text_from_json`, which assumes a dict: for field in self._TEXT_FIELDS: if field in data and isinstance(data[field], str): A JSON line does not have to be an object -- `"context"`, `["text"]` and `42` are all valid JSON. For those, `field in data` stops being a key lookup and becomes a substring/membership test, so `data[field]` raises: "context" -> "text" in "context" is True (substring!) -> TypeError: string indices must be integers ["text", "foo"] -> TypeError: list indices must be integers 42 -> TypeError: argument of type 'int' is not iterable The TypeError escapes past `except json.JSONDecodeError: continue`, so the whole load dies on one odd line. That except clause is also the tell: a *malformed* line is already skipped gracefully. A *well-formed* line that happens not to be an object should be too -- it carries no text either way. This makes the two agree. Reachable from `unsloth-cli.py:253` (`--dataset foo.jsonl` auto-detect) and `RawTextDataLoader` is exported from `unsloth/__init__.py`. Co-Authored-By: Claude Opus 4.8 (1M context) * Slim the non-object jsonl regression test and shorten the guard comment --------- Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Daniel Han --- tests/test_raw_text.py | 18 ++++++++++++++++++ unsloth/dataprep/raw_text.py | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/tests/test_raw_text.py b/tests/test_raw_text.py index ba16e0cfc4..18549adfe8 100644 --- a/tests/test_raw_text.py +++ b/tests/test_raw_text.py @@ -295,7 +295,25 @@ def test_smart_chunk_text_single_chunk_no_eos_returns_plain_list(): return True +def test_load_from_file_skips_non_object_json_lines(): + """Non-object .jsonl lines (valid JSON, not dicts) are skipped, not fatal.""" + # "context" contains "text", ["text"] holds it, 42 isn't iterable -- each + # would reach data[field] and raise TypeError without the isinstance guard. + with tempfile.NamedTemporaryFile("w", suffix = ".jsonl", delete = False) as f: + f.write('"context"\n["text", "x"]\n42\n{"text": "keep this"}\n') + path = f.name + try: + text = RawTextDataLoader(None)._read_file_by_format(path, "json_lines") + assert text == "keep this", text + finally: + os.unlink(path) + + print("test_load_from_file_skips_non_object_json_lines passed") + return True + + if __name__ == "__main__": success = test_raw_text_loader() success = test_smart_chunk_text_single_chunk_no_eos_returns_plain_list() and success + success = test_load_from_file_skips_non_object_json_lines() and success sys.exit(0 if success else 1) diff --git a/unsloth/dataprep/raw_text.py b/unsloth/dataprep/raw_text.py index 128d966ecd..8623285a25 100644 --- a/unsloth/dataprep/raw_text.py +++ b/unsloth/dataprep/raw_text.py @@ -236,6 +236,10 @@ class RawTextDataLoader: def _extract_text_from_json(self, data): """Extract text from JSON object using common field names.""" + # Skip non-object lines (str/list/number): `field in data` would be a + # substring/membership test, not a key lookup, and `data[field]` raises. + if not isinstance(data, dict): + return "" for field in self._TEXT_FIELDS: if field in data and isinstance(data[field], str): return data[field]