Net-Base Revista

13.08.2026

JSON em Delphi: analisador de stream rápido com System.JSON + armadilhas do UTF-8 com caracteres especiais

Quando payloads JSON em Delphi chegam diretamente de um Stream, o „fazer o parse rápido“ transforma‑se rapidamente num problema de produção: alto consumo de memória, erros de parsing esporádicos e caracteres acentuados corrompidos. Este artigo prático mostra como construir um parser de Stream rápido com System.JSON...

13.08.2026

Do tema da revista à prática do projeto

Páginas de serviços e técnicas correspondentes ao artigo

“JSON in Delphi“ soa como um problema resolvido: System.JSON está a bordo, REST-chamadas devolvem texto, pronto. Na prática, os erros reais porém surgem onde o JSON não está disponível como uma string confortável, mas como um Stream: HTTP-Response-Stream, Datei-Stream, Named Pipe, Message-Queue ou um grande BLOB na base de dados. Então somam-se três aspectos que no dia a dia são frequentemente subestimados: comportamento de memória, codificação de caracteres (especialmente UTF-8) e casos limites relacionados com caracteres especiais.

Este artigo mostra uma abordagem limpa e rápida para parsear JSON a partir de um TStream sem gerar cópias desnecessárias — e sobretudo sem as armadilhas típicas do UTF-8, em que caracteres acentuados ficam “quebrados” ou em que parsers falham esporadicamente com mensagens crípticas. O foco está nas implicações para operação e estabilidade das interfaces: depuração reproduzível, limites claros da abordagem e critérios para quando o esforço realmente compensa.

Por que Streams se comportam diferente ao parsear JSON em Delphi

Enquanto um documento JSON for pequeno, o caminho “ler Stream para String, depois parsear” é conveniente. A partir de um certo tamanho de payload (típico: listas grandes, relatórios, dados de sincronização, exports de log) isso fica caro:

  • Duplicação de memória: Você lê bytes para um buffer, converte para uma string Unicode (Delphi-String = UTF-16), e o parser cria internamente mais estruturas. Isso pode significar várias cópias temporárias.
  • Pressão no GC/heap: Muitas strings temporárias e valores JSON aumentam a fragmentação e a sobrecarga de alocação, especialmente em processos de longa duração (services, workers, jobs de importação).
  • O cenário de erro fica obscuro: Se durante a leitura já forem feitas suposições erradas sobre a codificação, o parser JSON vê apenas “caracteres estranhos” ou bytes de controle inesperados.

É importante uma separação clara: JSON é formalmente Unicode; na via de transporte é quase sempre UTF-8. Delphi trabalha internamente, porém, com UTF-16. A transição de bytes (Stream) para caracteres (String) é o ponto onde surgem problemas com caracteres especiais — não no JSON em si.

System.JSON: O que faz bem – e onde você deve ter cuidado

System.JSON é em Delphi o padrão para JSON baseado em DOM: você obtém um modelo de objetos (TJSONObject, TJSONArray), pode consultar valores, iterar, serializar. Isso é robusto para integrações de negócio típicas, mas tem duas consequências:

  • Não é um parser de streaming verdadeiro: O modelo de objetos é construído por completo. Você até pode evitar ler para uma string extra em alguns casos, mas o DOM continua a consumir muita memória.
  • A entrada do parser costuma ser texto: Dependendo da versão de Delphi e da API utilizada, rapidamente você volta a trabalhar com string, incluindo conversões de encoding.

Se o seu objetivo é um “parser de stream rápido”, na prática normalmente quer dizer uma de duas coisas: (1) evitar cópias desnecessárias e (2) falhar o mais cedo possível (fail-fast) em payloads corrompidos. Ambos são alcançáveis com System.JSON, desde que você controle a etapa de bytes-para-texto.

Armadiilhas do UTF-8 com caracteres especiais: as causas típicas

Gráfico abstrato mostra caracteres UTF-8 de múltiplos bytes atravessando limites de chunk e bufferização correta no decodificador antes da análise JSON
Chunking é inofensivo — desde que o decodificador UTF-8 armazene sequências multibyte através das fronteiras.

Se Umlaute (ä/ö/ü/ß) ou outros caracteres especiais no resultado aparecem incorretos (ä, – etc.), isso é quase sempre um mismatch de encoding. No ambiente Delphi essas causas são especialmente frequentes:

1) ANSI-Fallback durch „bequeme“ Helper

Alguns caminhos de leitura assumem tacitamente o encoding ANSI do sistema (codepage do sistema Windows), quando nenhuma codificação explícita é fornecida. Isso só chama atenção quando um payload contém mais que ASCII. Em dados de teste costuma estar “ok” por acaso; em produção dá problema com nomes reais, localidades, textos livres.

2) BOM-Verwirrung (Byte Order Mark)

UTF-8 pode começar com BOM (bytes EF BB BF). No contexto web o BOM é pouco comum, mas em arquivos ocorre. Alguns leitores detectam o BOM e ajustam a codificação, outros não ou apenas em modos específicos. Se um BOM entrar na string como caractere normal, você frequentemente verá um invisível “Zero Width No-Break Space” no início ou o parser JSON falhará já no primeiro token.

3) Doppel-Konvertierung (UTF-8 wird „nochmal“ interpretiert)

O padrão clássico de erro “ä” em vez de “ä” surge quando bytes UTF-8 foram primeiro corretamente decodificados para Unicode, mas depois são novamente interpretados de forma errada como bytes ANSI/UTF-8 (ou o contrário). Em Delphi isso acontece com frequência quando há conversões ambíguas entre TBytes, RawByteString e string.

4) Trunkierung mitten in einem Multibyte-Zeichen

UTF-8 codifica caracteres especiais em 2–4 bytes. Se você lê em chunks (p.ex. 8 KB) e o limite do chunk cai no meio do caractere, o decodificador precisa fazer o buffering corretamente. Uma abordagem ingênua que converte cada chunk separadamente em String e depois cola produz sequências inválidas. Isso pode aparecer como um erro “esporádico”, dependendo de limites de pacote, comportamento de proxy ou HTTP-Chunking.

5) Falsche Annahmen aus HTTP-Headern

No REST a fonte costuma ser Content-Type: application/json; charset=utf-8. Alguns servidores, porém, não enviam charset, outros enviam informações incorretas. Se você usar o header cegamente, isso pode variar conforme a versão do backend. Para operação e suporte é útil verificar o fluxo de bytes real e registrar nos logs em caso de erro.

Ein sauberer Ansatz: Stream → UTF-8-Decoder → JSON-Parser

A pipeline robusta consiste em três etapas claras:

  1. Ler bytes do stream (controlado, se necessário com limite/timeout no cliente HTTP).
  2. Decodificação para Unicode com UTF-8 explícito (tolerar BOM opcionalmente).
  3. Parsing com System.JSON para um modelo de objetos ou para extração direcionada.

O ponto de maior impacto é a etapa 2: você não quer que em algum lugar o “Default Encoding” decida. Em Delphi isso significa: TEncoding.UTF8 explizit setzen e não confiar em conversões implícitas.

Was „schnell“ hier konkret bedeutet

Com System.JSON você não vai „remover“ o DOM. Mas você pode evitar:

  • uma cópia adicional de toda a Payload como string intermediária, quando internamente você de qualquer forma só precisa de poucos valores (então vale mais a pena usar outro parser; sobre isso mais adiante),
  • reencodificações múltiplas,
  • e você pode ler Payloads muito grandes de forma controlada (com limite de tamanho e mensagem de erro clara), em vez de terminar em Out-of-Memory ou Access Violations.

O caso específico: caracteres especiais corrompidos, mas apenas às vezes

Um caso marginal da prática é particularmente traiçoeiro: a Payload é, em princípio, um JSON UTF-8 válido, mas você o lê em Chunks e converte por Chunk para String. Enquanto aparecer apenas ASCII, você não percebe nada. Assim que um Umlaut ficar exatamente na fronteira do Chunk, surgem sequências UTF-8 inválidas. Resultado: ou caracteres corrompidos ou um erro de parse em uma posição que não corresponde ao conteúdo real.

Como reconhecer isso:

  • Erros de parse ocorrem „aleatoriamente“ em respostas grandes, não nas pequenas.
  • A mesma requisição às vezes funciona, às vezes não (dependendo do Chunking/Transport).
  • Um hexdump dos bytes mostra UTF-8 válido, mas a sua string registrada contém Replacement Characters (�) ou o clássico Mojibake.

A solução não é usar „readln“ mais vezes ou „buffers maiores“; é utilizar um decodificador que faça o buffer correto de sequências multibyte através dos limites dos Chunks. É exatamente aí que TStreamReader em combinação com uma codificação UTF-8 pode ser útil — se você o inicializar corretamente.

Guia prático: verificar UTF-8 de forma reprodutível em Delphi

Configuração de depuração com impressões de dump de bytes e material de trabalho para verificar bytes UTF-8 e BOM em payloads JSON
Para uma depuração limpa, o fluxo de bytes vem primeiro: BOM, truncamento e sequências inválidas ficam visíveis rapidamente.

Antes de mexer no parser, você precisa de um setup de depuração que torne visível a sequência real de bytes. Para suporte e operação isso vale ouro, porque depois você poderá dizer claramente se a contraparte está entregando dados incorretos ou se sua pipeline está decodificando errado.

1) Verificar os primeiros bytes (BOM, início do JSON)

Se o JSON vier com BOM, você verá no início do stream EF BB BF. Logo depois deve tipicamente vir „{“ ou „[“. Se já aparecer „“ na string, o BOM não foi tratado como BOM, mas decodificado como texto.

2) Logar bytes brutos em Hex – mas de forma limitada

Não registre Payloads completos em produção (proteção de dados, custos, volume de logs). Provou ser eficaz:

  • Prefixo (por exemplo, primeiros 256 ou 1024 bytes),
  • Sufixo (últimos 256 bytes),
  • e um hash (SHA-256) para correlação, quando precisar comparar Payloads.

Com isso você pode muitas vezes diagnosticar problemas de caracteres especiais em minutos: A sequência de bytes para „ä“ está correta (C3 A4)? Há truncamento? Aparece um 0x00 (Nullbyte) inesperado, por exemplo por suposição errada de UTF-16?

3) Content-Type und Charset mitloggen

Em HTTP/REST: Registre o Content-Type e o charset declarado. Se os bytes forem claramente UTF-8, mas o charset afirmar outra coisa, você não deve seguir isso cegamente no cliente. Para JSON, UTF-8 é o padrão de fato. Em caso de dúvida: análise de bytes vence os headers.

Parser de Stream rápido com System.JSON: design sem cópias desnecessárias

Schemahafte Darstellung einer Pipeline aus Stream, UTF-8-Decoding, Größenlimit und JSON-DOM-Parsing in Delphi
Uma pipeline clara com limite e UTF-8 explícito separa transporte, decodificação e parsing de forma limpa.

Um padrão praxistauglich é: você lê do Stream para um buffer de bytes, constrói a partir dele uma string exatamente uma vez em UTF-8, e passa essa string ao JSON-Parser. Isso não é „Streaming“ no sentido de SAX, mas é uma pipeline controlada e performática sem trocas de encoding inesperadas.

Importante para a arquitetura: construa a função de modo que ela decida centralmente num único lugar:

  • Qual regra de encoding se aplica (em geral UTF-8, BOM opcional)?
  • Qual o tamanho máximo permitido para a Payload (proteção contra DoS, limite operacional)?
  • Como são as mensagens de erro (com contexto, mas sem vazamento de dados)?

Estratégia de leitura: buffer limitado em vez de „StreamToString“ sem limite

Se você aceita JSON de fontes externas (parceiros, clientes móveis, terceiros), um limite de tamanho é obrigatório. Sem limite, um único request infeliz basta para levar um serviço a Memory Pressure. Na prática isso significa: ao ler, contar a soma dos bytes e abortar ao atingir o limite — com uma exceção clara, compreensível no monitoramento.

Por que normalmente espero UTF-8 „sem BOM“, mas toleraria BOM

Em REST-Payloads o BOM aparece raramente. Em arquivos (exportações, edição manual) acontece com mais frequência. Para rotas de importação robustas, é sensato tolerar o BOM, mas registrá‑lo no log, pois pode ser um indício do „mundo de arquivo“ em vez do „mundo de API“.

Os assassinos silenciosos: TStreamReader-Defaults e leitores de texto em operação mista

TStreamReader é conveniente, mas você precisa controlar duas coisas de forma rígida:

  • Definir explicitamente o encoding: Não espere que ele o „reconheça“.
  • Entender o buffering: O reader faz buffering internamente. Se você ler o mesmo Stream mais adiante em outro componente, a posição importa. Isso soa trivial, mas em pipelines de importação maiores torna‑se rapidamente uma fonte de erros.

Particularmente problemático é o uso misto: primeiro ler uma parte como bytes (por exemplo para registro ou bytes mágicos), depois continuar com TStreamReader. Se você não rebobinar corretamente ou inicializar o reader na posição certa, você acaba lendo a partir do byte 257 em vez de 0. Der JSON-Parser meldet dann „Invalid character at position …“, obwohl die Payload an sich korrekt ist.

Quando caracteres especiais continuam „quebrados“ apesar de UTF-8: Escaping vs. Unicode real

O JSON pode conter caracteres especiais de duas formas:

  • Como caracteres UTF-8 reais (por exemplo „München“ como bytes C3 BC …).
  • Como sequência de escape (p. ex. „Mu00fcnchen“).

Ambas são válidas. Para a prática é importante: sequências de escape evitam muitos problemas de transporte, mas apenas mascaram erros de codificação aparentemente. Se o seu sistema interpretar bytes incorretamente em algum ponto, isso é um risco operacional, não apenas um bug cosmético. Além disso, sequências de escape podem confundir logging/monitoramento quando as equipas esperam ver “texto legível”.

System.JSON fornece em ambos os casos, no fim, strings Delphi normais (UTF-16), desde que o caminho até aí tenha sido correto.

Avaliar a performance realisticamente: custos do DOM, arrays grandes e seletividade

O maior ganho de performance muitas vezes não é “tornar o parser mais rápido”, mas analisar menos. Com System.JSON isso é difícil, porque você obtém o DOM. Três situações típicas:

  • Arrays grandes (10.000+ elementos): A construção do DOM consome tempo e RAM. Se você precisa apenas de 2 campos por elemento, um parser com suporte a streaming (SAX/Tokenizer) costuma ser mais adequado. System.JSON não foi feito para isso.
  • Objetos individuais com muitos campos: Se você precisa apenas de alguns campos, ainda pode usar o DOM, mas evite percorrê-lo várias vezes. Recupere os valores uma vez e mapeie-os para suas estruturas.
  • Múltiplas cargas grandes consecutivas: Em tarefas de importação ou workers de sincronização, vale a pena encapsular o parsing numa etapa clara e, após cada documento, liberar todas as referências para que o gerenciador de memória possa limpar. Isso é trivial, mas em serviços costuma ser feito como uma atividade secundária.

Um “parser de stream rápido” com System.JSON é, portanto, muitas vezes um bom compromisso: ler de forma eficiente e correta, usar o DOM de forma consciente, definir limites. Se você precisa de semântica de streaming real (p. ex. processar elementos de um array um a um, sem manter tudo na memória), System.JSON não é a base adequada.

Robustez em operação: padrões de erro e como torná-los imediatamente acionáveis

Erros de parsing de JSON frequentemente não ajudam nos logs porque mencionam apenas uma posição. Para operação e suporte você precisa de contexto:

  • Posição em bytes vs. posição em caracteres: Em UTF-8 isso não é idêntico. Se o parser reporta uma posição em caracteres, a posição em bytes pode diferir. Para dumps em bytes, a posição em bytes é decisiva.
  • Trecho em torno do local do erro: Registre, em caso de erro, uma pequena janela em torno da posição (p. ex. 40 caracteres antes/depois), mas apenas se não houver dados sensíveis. Alternativamente: registre apenas em hexadecimal.
  • Correlação: Request-ID, endpoint, Partner-ID, hash do payload. Caso contrário você nunca reencontrará “aquele erro”.

O objetivo é que, após um incidente em produção, você consiga responder em poucos minutos: “codificação interpretada incorretamente”, “payload truncado”, “servidor entrega invalid JSON” ou “temos um problema de mapeamento”.

Evitar armadilhas do UTF-8 de forma direcionada: lista de verificação

  • Codificação sempre explícita: Ao ler de streams e ao escrever em logs/arquivos, não confie nos valores padrão.
  • Sem conversão chunk-para-string: Se você lê em chunks, acumule bytes ou use um decodificador que faça buffer de sequências multibyte.
  • Tolerante ao BOM, mas detectável: Aceitar, mas ser capaz de reconhecê-lo no debug.
  • Definir limites: Tamanho máximo do payload, profundidade máxima de objeto/array (se você puder controlar isso), timeouts no cliente HTTP.
  • Responsabilidades separadas: „ler o transporte“ e „analisar JSON“ encapsular separadamente. Isso permite depuração mais rápida e a substituição do parser posteriormente.
  • Quando realmente compensa o esforço para um parser em streaming?

    Você não precisa otimizar toda ocorrência de JSON. A abordagem costuma compensar tipicamente quando pelo menos uma das seguintes situações se aplica:

    • Payloads grandes (vários MB) ocorrem com regularidade ou podem ocorrer.
    • Processos de longa execução (Service, Worker) processam muitos payloads e você observa picos de memória ou fragmentação.
    • Interop com sistemas heterogêneos: vários parceiros, plataformas diferentes, ocasionalmente encodings com erros.
    • Histórico de incidentes: já houve caracteres acentuados corrompidos (Umlaute), erros de parse esporádicos ou interrupções de importação de difícil reprodução.

    Se seus payloads são pequenos e vêm de uma fonte controlada, muitas vezes um caminho simples basta – mas mesmo assim: definir explicitamente UTF-8 custa quase nada e evita surpresas posteriores.

    Delimitação: quando você realmente precisa de streaming

    System.JSON é orientado a DOM. Se você quer processar dados verdadeiramente “em passagem”, por exemplo um grande array elemento a elemento, sem mantê‑lo completo na memória, precisa de outra abordagem de parser (Tokenizer/SAX). Isso não é um juízo de valor, mas uma decisão de arquitetura:

    • DOM (System.JSON): conveniente, adequado para objetos de negócio típicos, mas com alto consumo de memória.
    • Streaming/SAX: menor uso de memória, adequado para dados muito grandes, mas exige mais esforço de implementação e um tratamento de erros mais cuidadoso.

    Um compromisso sensato costuma ser: implementar corretamente o tratamento de fluxo, a codificação e os limites, e só então decidir se o DOM ainda é adequado. Em muitos projetos, isso por si só estabiliza significativamente o funcionamento.

    Conclusão: JSON in Delphi será confiável, se você tratar codificação e streams como uma camada própria

    A maioria dos problemas em torno de „JSON in Delphi“ não está relacionada ao parser JSON em si, mas ao trecho discreto anterior: bytes de um stream são transformados em texto. Se você tratar explicitamente UTF-8 ali, reconhecer casos de BOM, não ignorar limites de chunk e definir limites de tamanho claros, os erros típicos com caracteres especiais desaparecem – e problemas de parse esporádicos tornam‑se reproduzíveis.

    System.JSON continua a ser um padrão pragmático: não é o parser de streaming mais rápido, mas é sólido se você controla a entrada e aceita conscientemente os custos do DOM. Se desejar, podemos percorrer juntos seu caminho de importação/REST e identificar o ponto em que a codificação ou o chunking falham: entrar em contato.

    Para este tema, analisadores de stream JSON também são importantes. O artigo enquadra esses aspectos de forma compreensível e mostra no que realmente importa no dia a dia.

    Discutir projeto ou iniciativa de modernização com Net-Base.

    Próximo passo

    Quando o tema se tornar um projeto real, arquitetura, ambiente existente e operação devem ser considerados em conjunto desde o início.

    Não apenas apoiamos questões pontuais, mas também quando fragmentos de código-fonte, temas legados ou ideias de portais precisam evoluir para um projeto empresarial robusto.

    • Estado atual, estado-alvo e riscos técnicos são avaliados em conjunto.
    • REST, o acesso a dados, os portais e o Rollout não são adiados para fases posteriores.
    • Você identifica cedo qual caminho é viável econômica e operacionalmente.

    Partilhar publicação

    Compartilhar esta publicação diretamente

    LinkedIn, X, XING, Facebook, WhatsApp e E‑Mail estão disponíveis imediatamente. Para o Instagram, preparamos diretamente o link e o texto curto.

    E-mail

    O Instagram abre numa nova aba. O link e o texto curto são copiados previamente para a área de transferência.