Opened 3 weeks ago
Last modified 6 days ago
#37442 assigned Bug
JSONL deserializer incorrectly treats Unicode line separators as record boundaries
| Reported by: | Patel Namraa | Owned by: | Patel Namraa |
|---|---|---|---|
| Component: | Core (Serialization) | Version: | 5.2 |
| Severity: | Normal | Keywords: | |
| Cc: | Patel Namraa | Triage Stage: | Accepted |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
The JSONL deserializer incorrectly uses str.splitlines() to split string and bytes input into records. splitlines() treats U+2028 (LINE SEPARATOR), U+2029 (PARAGRAPH SEPARATOR), and U+0085 (NEXT LINE) as line boundaries, even though these characters can occur unescaped inside JSON strings.
The JSONL serializer uses ensure_ascii=False, so these characters can be emitted directly in serialized JSON. As a result, data produced by Django's JSONL serializer cannot always be deserialized again when a serialized value contains one of these characters.
For example:
from django.core import serializers
from myapp.models import Author
author = Author(pk=1, name="Jane\u2028Doe")
data = serializers.serialize("jsonl", [author])
list(serializers.deserialize("jsonl", data))
This raises DeserializationError because the record is split at U+2028 instead of only at the JSONL record delimiter.
The same behavior occurs with U+2029 and U+0085, and with both str and bytes input.
The JSONL serializer uses \n as the record separator, so the deserializer should split only on \n rather than using splitlines().
A possible fix is:
stream_or_string = stream_or_string.split("\n")
This continues to support CRLF input because the resulting line retains a trailing \r, which is valid JSON whitespace.
I have added regression tests covering U+2028, U+2029, and U+0085 with both str and bytes input, as well as a test confirming that CRLF line endings continue to work.
The issue does not affect loading JSONL from a file object in the same way, since file iteration uses the actual newline delimiter. However, it affects callers passing serialized JSONL as a string or bytes object to serializers.deserialize().
I have reproduced the issue against Django's current development version.
Change History (4)
comment:1 by , 3 weeks ago
comment:2 by , 3 weeks ago
| Has patch: | unset |
|---|---|
| Triage Stage: | Unreviewed → Accepted |
| Version: | dev → 6.1 |
comment:3 by , 3 weeks ago
| Version: | 6.1 → 5.2 |
|---|
comment:4 by , 6 days ago
| Has patch: | set |
|---|
Thanks for the report. Regression in ee5147cfd7de2add74a285537a8968ec074e70cd (Django 5.2)
diff --git a/tests/serializers/test_jsonl.py b/tests/serializers/test_jsonl.py index 3137b037a9..98eb2edc15 100644 --- a/tests/serializers/test_jsonl.py +++ b/tests/serializers/test_jsonl.py @@ -8,7 +8,7 @@ from django.db import models from django.test import TestCase, TransactionTestCase from django.test.utils import isolate_apps -from .models import Score +from .models import Score, Category from .tests import SerializersTestBase, SerializersTransactionTestBase @@ -99,6 +99,17 @@ class JsonlSerializerTestCase(SerializersTestBase, TestCase): ): list(serializers.deserialize("jsonl", test_string)) + def test_unicode_line_separators(self): + for separator in ("\u0085", "\u2028", "\u2029"): + name = f"Jane{separator}Doe" + data = serializers.serialize("jsonl", [Category(pk=1, name=name)]) + for value in (data, data.encode()): + with self.subTest(separator=repr(separator), input_type=type(value)): + objects = list(serializers.deserialize("jsonl", value)) + self.assertEqual(len(objects), 1) + self.assertEqual(objects[0].object.name, name) + + def test_helpful_error_message_invalid_field(self): """ If there is an invalid field value, the error message contains the