Net-Base Журнал

13.08.2026

JSON в Delphi: высокопроизводительный потоковый парсер с использованием System.JSON + подводные камни UTF-8 при специальных символах

Когда JSON-пейлоады в Delphi поступают непосредственно из потока, попытка «быстро распарсить» вскоре превращается в проблему в продуктивной среде: высокое потребление памяти, периодические ошибки парсинга и искажённые умлауты. В этой практической статье показано, как с помощью System.JSON реализовать быстрый потоковый парсер...

13.08.2026

От темы в журнале к проектной практике

Соответствующие страницы услуг и технологий к статье

«JSON in Delphi» звучит как решённая задача: System.JSON присутствует, REST-вызовы возвращают текст, и всё. На практике же реальные ошибки возникают там, где JSON доступен не как удобная строка, а как поток: HTTP-Response-Stream, файловый поток, Named Pipe, Message-Queue или большой BLOB из базы данных. Тогда сходятся три фактора, которые в повседневной работе часто недооценивают: поведение в отношении памяти, кодировка символов (в частности UTF-8) и пограничные случаи, связанные со специальными символами.

В этой статье показан аккуратный, быстрый подход к парсингу JSON из TStream без создания лишних копий — и прежде всего без типичных ловушек UTF-8, при которых умляуты «сломаны» или парсер периодически выходит с криптическими сообщениями. Фокус направлен на влияние для эксплуатации и стабильности интерфейсов: воспроизводимая отладка, чёткие границы подхода и критерии, когда затраты действительно оправданы.

Почему потоки при парсинге JSON в Delphi ведут себя иначе

Пока JSON-документ небольшой, путь «прочитать поток в строку, затем распарсить» удобен. Но при определённом размере полезной нагрузки (типично: большие списки, отчёты, данные синхронизации, экспорты логов) это становится дорого:

  • Дублирование в памяти: вы читаете байты в буфер, преобразуете в Unicode-строку (Delphi-String = UTF-16), парсер создаёт внутренние структуры. Это может означать несколько копий одновременно.
  • Давление на GC/кучу: множество временных строк и JSON-значений увеличивает фрагментацию и накладные расходы на выделение памяти, особенно в длительно работающих процессах (службы, воркеры, задания импорта).
  • Картинка ошибок становится неясной: если при чтении уже сделаны неверные предположения о кодировке, JSON-парсер увидит только «странные символы» или неожиданные управляющие байты.

Важно чёткое разделение: формально JSON — Unicode, по каналу он почти всегда UTF-8. Delphi же внутри работает с UTF-16. Переход от байтов (Stream) к символам (String) — это та точка, где возникают проблемы со специальными символами, а не сам JSON.

System.JSON: что он хорошо умеет — и на что вам следует обратить внимание

System.JSON в Delphi является стандартом для DOM-ориентированного JSON: вы получаете объектную модель (TJSONObject, TJSONArray), можете запрашивать значения, итерировать, сериализовать. Это надёжно для типичных бизнес-интеграций, но имеет два следствия:

  • Это не настоящий потоковый парсер: объектная модель строится полностью. Вы, возможно, экономите на чтении в дополнительную строку, но DOM остаётся ресурсоёмким.
  • Вход парсера обычно является текстом: в зависимости от версии Delphi и используемого API вы быстро снова окажетесь со строкой, включая конвертацию кодировок.

Если ваша цель — «быстрый потоковый парсер», то на практике это обычно означает одно из двух: (1) отсутствие ненужных копий и (2) максимально ранний режим fail-fast при повреждённых полезных нагрузках. Обе цели достижимы с System.JSON, если вы контролируете этап преобразования байтов в текст.

Ловушки UTF-8 при специальных символах: типичные причины

Abstrakte Grafik zeigt UTF-8-Mehrbytezeichen über Chunk-Grenzen und korrekte Pufferung im Decoder vor dem JSON-Parsing
Chunking безопасен — если UTF-8‑декодер буферизует многобайтовые последовательности через границы.

Если умляуты (ä/ö/ü/ß) или другие специальные символы в результате выглядят неправильно (ä, – и т. п.), это почти всегда несоответствие кодировок. В окружении Delphi такие причины встречаются особенно часто:

1) ANSI‑Fallback из‑за «удобных» хелперов

Некоторые способы чтения по умолчанию тихо используют системную ANSI‑кодировку (кодовую страницу Windows‑системы), если явно не передана кодировка. Это проявляется только тогда, когда полезная нагрузка содержит не только ASCII. В тестовых данных это часто случайно работает, а в продакшене приводит к сбоям при реальных именах, местах, свободных текстах.

2) Путаница с BOM (Byte Order Mark)

UTF-8 может начинаться с BOM (байты EF BB BF). В вебе BOM встречается редко, но в файлах бывает. Некоторые ридеры распознают BOM и подстраивают кодировку, другие — нет или только в определённых режимах. Если BOM попадает в строку как обычный символ, вы часто увидите невидимый «Zero Width No-Break Space» в начале или JSON‑парсер сразу упадёт на первом токене.

3) Двойная конвертация (UTF-8 интерпретируют «ещё раз»)

Классическая картина «ä вместо ä» возникает, когда UTF-8‑байты сначала корректно декодированы в Unicode, но позже снова ошибочно интерпретируются как ANSI/UTF-8‑байты (или наоборот). В Delphi это часто происходит при неясных конвертациях между TBytes, RawByteString и string.

4) Усечение в середине многобайтового символа

UTF-8 кодирует специальные символы в 2–4 байта. Если вы читаете чанки (например, по 8 KB) и граница чанка попадает в середину символа, декодер должен корректно буферизовать оставшиеся байты. Наивный подход, который каждый чанк отдельно преобразует в строку и затем склеивает, порождает недопустимые последовательности. Это выглядит как «спорадическая» ошибка, зависящая от границ пакетов, поведения прокси или HTTP‑чанкинга.

5) Неверные предположения по HTTP‑хедерам

При REST источником часто является Content-Type: application/json; charset=utf-8. Но некоторые серверы не указывают charset, некоторые указывают неверно. Если слепо полагаться на заголовок, поведение может меняться в зависимости от версии бэкенда. Для эксплуатации и поддержки полезно проверить фактический байтовый поток и при ошибке залогировать его.

Чёткий подход: Stream → UTF-8‑Decoder → JSON‑Parser

Надёжная конвейерная схема состоит из трёх чётких этапов:

  1. Чтение байтов из потока (контролируемо, при необходимости с лимитом/таймаутом в HTTP‑клиенте).
  2. Декодирование в Unicode с явным указанием UTF-8 (BOM можно опционально допускать).
  3. Парсинг через System.JSON в объектную модель или для целевой выборки.

Ключевой рычаг — этап 2: вы не хотите, чтобы где‑то решал «Default Encoding». В Delphi это означает: явно установить TEncoding.UTF8 и не полагаться на неявные конвертации.

Что здесь конкретно означает „schnell“

С помощью System.JSON вы не избавитесь от DOM. Зато можно избежать:

  • создания дополнительной копии всей полезной нагрузки в виде промежуточной строки, если вам внутри нужны лишь отдельные значения (в таких случаях обычно лучше другой парсер; об этом ниже),
  • многократного перекодирования,
  • и вы можете контролируемо читать очень большие полезные нагрузки (с ограничением размера и корректным сообщением об ошибке), вместо того чтобы завершиться Out-of-Memory или Access Violations.

Конкретный пограничный случай: спецсимволы повреждаются, но только иногда

Один практический пограничный случай особенно коварен: полезная нагрузка в целом является корректным UTF-8-JSON, но вы читаете её чанками и конвертируете каждый чанк в строку. Пока встречается только ASCII, вы ничего не заметите. Как только диакритический символ (умлаут) оказывается ровно на границе чанка, появляются некорректные UTF-8-последовательности. В результате — либо повреждённые символы, либо ошибка парсера в месте, не соответствующем реальному содержимому.

Как это распознать:

  • Ошибки парсинга возникают «случайно» при больших ответах, а не при маленьких.
  • Тот же запрос иногда проходит, иногда нет (в зависимости от chunking/транспорта).
  • Hex-dump байтов показывает корректный UTF-8, но в вашем залогированном строковом представлении появляются Replacement Characters (�) или классический Mojibake.

Решение не в том, чтобы «больше readln» или «большие буферы» использовать, а в том, чтобы применять декодер, который корректно буферизует многобайтовые последовательности через границы чанков. Именно здесь TStreamReader в сочетании с UTF-8-encoding может быть полезен — при условии правильной инициализации.

Практическое руководство: воспроизводимая проверка UTF-8 в Delphi

Набор для отладки с распечатками дампов байтов и материалами для проверки UTF-8-байтов и BOM в JSON-пейлодах
Для корректной отладки первым делом важен байтовый поток: BOM, усечение и некорректные последовательности быстро становятся видимыми.

Прежде чем менять парсер, соберите отладочный набор, который делает фактическую последовательность байтов видимой. Для поддержки и эксплуатации это бесценно: впоследствии вы сможете однозначно сказать, передаёт ли контрагент неверные данные или же ваша конвейер декодирует неправильно.

1) Проверить первые байты (BOM, начало JSON)

Если JSON приходит с BOM, в начале потока вы увидите EF BB BF. Сразу после этого обычно должен идти „{“ или „[„. Если в строке уже присутствует «ï»»¿»» (), значит BOM не был распознан как BOM и был декодирован как текст.

2) Логировать сырые байты в шестнадцатеричном виде — но ограниченно

Не логируйте в продакшене полные полезные нагрузки (конфиденциальность, затраты, объём логов). Эффективно показывают себя:

  • префикс (например первые 256 или 1024 байта),
  • суффикс (последние 256 байт),
  • и хеш (SHA-256) для корреляции, если нужно сравнивать полезные нагрузки.

Это позволяет в большинстве случаев за пару минут понять проблему со спецсимволами: корректна ли байтовая последовательность для «ä» (C3 A4)? Имеется ли усечение? Появляется ли неожиданный 0x00 (нулевой байт), например из-за ошибочной интерпретации как UTF-16?

3) Content-Type und Charset mitloggen

При HTTP/REST: логируй den Content-Type и объявленный charset. Wenn die Bytes klar UTF-8 sind, aber charset etwas anderes behauptet, solltest du im Client nicht blind folgen. Для JSON ist UTF-8 der De-facto-Standard. Im Zweifel: Byte-Analyse gewinnt gegen Header.

Быстрый парсер потоков с System.JSON: проектирование без лишних копий

Схематичное изображение конвейера из Stream, UTF-8-декодирования, ограничения размера и JSON-DOM-парсинга в Delphi
Чёткая конвейерная схема с лимитом и явным UTF-8 чётко разделяет транспорт, декодирование и парсинг.

Практичный шаблон ist: Du liest aus dem Stream in einen Byte-Puffer, baust daraus einen String genau einmal mit UTF-8, und gibst diesen String an den JSON-Parser. Das ist nicht „Streaming“ im Sinne von SAX, aber es ist eine kontrollierte, performante Pipeline ohne überraschende Encoding-Wechsel.

Важно для архитектуры: Baue die Funktion so, dass sie an einer Stelle zentral entscheidet:

  • Welche Encoding-Regel gilt (in der Regel UTF-8, BOM optional)?
  • Wie groß darf die Payload maximal sein (DoS-Schutz, Betriebsgrenze)?
  • Wie sehen Fehlermeldungen aus (mit Kontext, aber ohne Datenleak)?

Стратегия чтения: ограниченное буферизование statt „StreamToString“ ohne Limit

Wenn du JSON aus externen Quellen annimmst (Partner, mobile Clients, Drittanbieter), ist ein Größenlimit Pflicht. Ohne Limit reicht ein einzelner unglücklicher Request, um einen Service in Memory Pressure zu bringen. Praktisch heißt das: beim Lesen die Summe der Bytes zählen und ab einer Grenze abbrechen – mit einer klaren Exception, die im Monitoring verständlich ist.

Почему ich bei UTF-8 oft „ohne BOM“ erwarte, aber BOM tolerieren würde

Bei REST-Payloads kommt BOM selten vor. Bei Dateien (Exports, manuelle Bearbeitung) dagegen öfter. Für robuste Importstrecken ist es sinnvoll, BOM zu tolerieren, aber im Log sichtbar zu machen, weil es ein Hinweis auf „Datei-Welt“ statt „API-Welt“ sein kann.

Тихие угрозы: TStreamReader-Defaults und Text-Reader im Mischbetrieb

TStreamReader удобен, aber du musst zwei Dinge sauber im Griff haben:

  • Явно задавать кодировку: Nicht hoffen, dass er es „erkennt“.
  • Понимать буферизацию: Der Reader puffert intern. Wenn du denselben Stream später nochmal woanders liest, ist die Position relevant. Das klingt trivial, wird aber in größeren Importpipelines schnell eine Fehlerquelle.

Особенно unangenehm ist Mischbetrieb: Erst ein Teil als Bytes lesen (z. B. für Logging oder Magic-Bytes), dann mit TStreamReader weiter. Wenn du dabei nicht sauber zurückspulst oder die Reader-Initialisierung an der richtigen Position machst, liest du ab Byte 257 statt ab 0. Der JSON-Parser meldet dann „Invalid character at position …“, obwohl die Payload an sich korrekt ist.

Когда спецсимволы остаются „kaputt“ trotz UTF-8: Escaping vs. echtes Unicode

JSON kann Sonderzeichen auf zwei Arten enthalten:

  • В виде настоящих UTF-8-символов (z. B. „München“ als Bytes C3 BC …).
  • В виде escape-последовательности (z. B. „Mu00fcnchen“).

Оба варианта допустимы. Для практики важно: escape-последовательности обходят многие транспортные проблемы, но они лишь кажуще скрывают ошибки кодировки. Если ваше System irgendwo байты неправильно интерпретирует, это риск для эксплуатации, а не просто косметический баг. Кроме того, escape-последовательности могут вводить в заблуждение при логировании/мониторинге, если команды ожидают „читаемый текст“.

System.JSON liefert dir in beiden Fällen am Ende normale Delphi-Strings (UTF-16), sofern der Weg bis dahin korrekt war.

Реалистичная оценка производительности: накладные расходы DOM, большие массивы и селективность

Самый сильный рычаг оптимизации производительности часто не в том, чтобы «ускорить парсер», а в том, чтобы меньше парсить. С System.JSON это сложно, потому что вы получаете DOM. Три типичных ситуации:

  • Große Arrays (10.000+ Elemente): построение DOM требует времени и оперативной памяти. Если вам нужны только 2 поля на элемент, имеет смысл использовать стриминговый парсер (SAX/Tokenizer). System.JSON для этого не предназначен.
  • Einzelobjekte mit vielen Feldern: если вам нужны только несколько полей, вы всё ещё можете использовать DOM, но избегайте многократного обхода. Получайте значения один раз и сопоставляйте их со своими структурами.
  • Mehrere große Payloads hintereinander: в импортных задачах или Sync-Worker’ах имеет смысл инкапсулировать парсинг в отдельный шаг и после каждого документа освобождать все ссылки, чтобы менеджер памяти мог очистить ресурсы. Это банально, но в сервисах это часто делают «по ходу».

«Быстрый потоковый парсер» на базе System.JSON часто является хорошим компромиссом: читать эффективно и корректно, осознанно использовать DOM, чётко определять границы. Если вам нужна настоящая семантика стриминга (например, обрабатывать элементы массива по одному, не удерживая всё в памяти), System.JSON не является подходящей основой.

Надёжность в эксплуатации: сценарии ошибок и как сделать их сразу видимыми

Ошибки парсинга JSON в логах часто малоинформативны, потому что они указывают лишь позицию. Для эксплуатации и поддержки вам нужен контекст:

  • Позиция в байтах vs. позиция в символах: при UTF-8 это не одно и то же. Если парсер сообщает позицию в символах, байтовая позиция может отличаться. Для дампов байтов решающим является байтовая позиция.
  • Фрагмент вокруг места ошибки: в случае ошибки логируйте небольшое окно вокруг позиции (например, 40 символов до/после), но только если там нет чувствительных данных. В качестве альтернативы логируйте только в шестнадцатеричном виде.
  • Корреляция: Request-ID, Endpoint, Partner-ID, Payload-Hash. Иначе вы никогда не найдёте «ту самую ошибку» снова.

Цель состоит в том, чтобы после инцидента в продакшене вы могли в течение нескольких минут ответить: „Encoding falsch interpretiert“, „Payload abgeschnitten“, „Server liefert invalid JSON“ oder „wir haben ein Mapping-Problem“.

Избегание подводных камней UTF-8: контрольный список

  • Кодировку всегда указывать явно: при чтении из потоков и при записи в логи/файлы не полагайтесь на значения по умолчанию.
  • Не делать конвертацию чанков в строку: если вы читаете чанками, собирайте байты или используйте декодер, который буферизует многобайтовые последовательности.
  • BOM терпимы, но должны быть видимы: принимать, но уметь распознавать при отладке.
  • Устанавливайте лимиты: макс. размер Payload, макс. глубина объектов/массивов (если вы можете это контролировать), таймауты в HTTP-клиенте.
  • Разделение обязанностей: „чтение транспорта“ и „парсинг JSON“ инкапсулировать отдельно. Так отлаживать быстрее и позже можно заменить парсер.
  • Когда действительно оправдана затрата на потоковый парсер?

    Не обязательно оптимизировать каждую точку работы с JSON. Этот подход обычно имеет смысл, если выполняется хотя бы одно из ниже перечисленных условий:

    • Большие Payloads (несколько МБ) встречаются регулярно или могут появиться.
    • Долгоживущие процессы (Service, Worker) обрабатывают много payloads, и вы наблюдаете всплески памяти или фрагментацию.
    • Взаимодействие с гетерогенными системами: несколько партнёров, разные платформы, периодически некорректные кодировки.
    • История инцидентов: уже были „сломанные Umlaute“, спорадические ошибки парсинга или трудно воспроизводимые прерывания импорта.

    Если ваши payloads небольшие и поступают из контролируемого источника, часто хватает простого подхода — но даже в этом случае: явная установка UTF-8 стоит почти ничего и предотвращает будущие сюрпризы.

    Уточнение: когда требуется настоящий потоковый парсинг

    System.JSON ориентирован на DOM. Если вы хотите обрабатывать данные действительно «на лету», например большое массив по элементам, не удерживая его целиком в памяти, нужен другой подход к парсингу (Tokenizer/SAX). Это не оценка, а архитектурное решение:

    • DOM (System.JSON): удобно, подходит для типичных бизнес-объектов, но требует много памяти.
    • Streaming/SAX: низкое потребление памяти, подходит для очень больших объёмов данных, но требует больших усилий при реализации и более тщательной обработки ошибок.

    Часто разумным компромиссом является: аккуратно реализовать обработку потоков, кодировку и ограничения, а уже затем решать, подходит ли DOM. Во многих проектах именно это существенно стабилизирует эксплуатацию.

    Вывод: JSON in Delphi становится надёжным, если вы рассматриваете кодировку и потоки как отдельный слой

    Большинство проблем, связанных с „JSON in Delphi“, не связаны с самим JSON-парсером, а с неявным участком перед ним: байты из потока превращаются в текст. Если явно обрабатывать UTF-8, распознавать случаи с BOM, не игнорировать границы чанков и задавать чёткие ограничения по размеру, типичные ошибки с особыми символами исчезают — а спорадические ошибки парсинга становятся воспроизводимыми.

    System.JSON остаётся прагматичным стандартом: не самый быстрый потоковый парсер, но надёжный, если вы контролируете вход и осознанно принимаете затраты DOM. Если хотите, мы можем вместе пройти ваш конкретный путь импорта/REST и выявить место, где ломается кодировка или нарезка чанков — свяжитесь с нами.

    Для этой темы также важны потоковые JSON-парсеры. Статья систематизирует эти аспекты и показывает, на что обращать внимание в повседневной практике.

    Обсудить проект или модернизацию с Net-Base.

    Следующий шаг

    Если из темы становится реальный проект, архитектуру, существующее состояние и эксплуатацию следует рассматривать совместно на ранней стадии.

    Мы поддерживаем не только при отдельных вопросах, но и тогда, когда из фрагментов исходного кода, унаследованных проблем или идей портала должен сформироваться надёжный корпоративный проект.

    • Текущее состояние, целевое состояние и технические риски оцениваются совместно.
    • REST, доступ к данным, порталы и развертывание не переносятся на более поздние этапы.
    • Вы заранее видите, какой путь экономически и операционно жизнеспособен.

    Поделиться записью

    Поделиться этой записью напрямую

    LinkedIn, X, XING, Facebook, WhatsApp и электронная почта доступны немедленно. Для Instagram мы готовим ссылку и краткий текст.

    Электронная почта

    Instagram открывается в новой вкладке. Ссылка и короткий текст предварительно копируются в буфер обмена.