NUL sanitization corrupts escaped text in annotation drafts

Author: HaokaiDingCreated Sep 16, 2026Updated Sep 16, 2026

Describe the bug

sanitize_null_bytes corrupts valid literal backslash text in draft results. A backslash followed by u0000n becomes a newline; a backslash followed by u0000 raises JSONDecodeError.

The helper is called by AnnotationDraft.save() and draft bulk import. It serializes the payload to JSON and removes NUL-looking escape substrings, including occurrences inside escaped backslashes. This leaves an incomplete escape or changes the remaining characters into a different escape.

To Reproduce

From a checkout, set PYTHONPATH=label_studio and run:

python
from tasks.result_utils import sanitize_null_bytes

text = 'JSON escape: ' + chr(92) + 'u0000n'
payload = [{
    'id': 'r1',
    'from_name': 'transcription',
    'to_name': 'text',
    'type': 'textarea',
    'value': {'text': [text]},
}]
cleaned = sanitize_null_bytes(payload)
print(repr(cleaned[0]['value']['text'][0]))
assert cleaned[0]['value']['text'][0] == text

Actual: the text contains a real newline after JSON escape: and the assertion fails. Changing the text to chr(92) + 'u0000' raises JSONDecodeError: Unterminated string.

Expected behavior

Only actual U+0000 characters should be removed. Literal backslash-u text, including in dictionary keys, should remain unchanged. Actual NUL characters still need sanitizing before PostgreSQL writes.

Environment

  • Current develop source, reporting Label Studio 1.24.0.dev0.
  • Python 3.13.15, macOS 26.6.2 ARM64.
  • Reproduced directly through the real helper; no Django server or database was used for this reproduction.

Additional context

The existing tests cover actual NUL removal but not escaped-backslash text. New regressions reproduce both the exception and silent text/key corruption, while retaining controls for actual NUL characters next to backslashes.

Source: HumanSignal/label-studio