Net-Base Revista

13.08.2026

JSON en Delphi: analizador de flujo rápido con System.JSON y trampas de UTF-8 en caracteres especiales

Si los payloads JSON en Delphi provienen directamente de un flujo, lo que a primera vista es «analizar rápidamente» se convierte pronto en un problema de producción: elevado consumo de memoria, fallos de parseo esporádicos y caracteres con diéresis (Umlauts) corruptos. Este artículo práctico muestra cómo, con System.JSON, construir un analizador de flujo rápido.

13.08.2026

Del tema de la revista a la práctica del proyecto

Páginas de servicios y técnicas relacionadas

„JSON in Delphi“ suena como un problema resuelto: System.JSON está presente, REST-Calls entregan texto, listo. En la práctica, los errores reales aparecen donde el JSON no se presenta como una cadena cómoda, sino como un stream: HTTP-Response-Stream, flujo de archivo, Named Pipe, Message-Queue o un gran BLOB desde la base de datos. Entonces confluyen tres aspectos que a menudo se subestiman en el día a día: comportamiento de memoria, codificación de caracteres (en particular UTF-8) y casos límite relativos a caracteres especiales.

Esta entrada muestra un enfoque limpio y rápido para parsear JSON desde un TStream sin generar copias innecesarias —y, sobre todo, sin las típicas trampas de UTF-8 en las que los Umlaute quedan “caputt” o los parsers fallan de forma esporádica con mensajes crípticos. El foco está en las implicaciones para la operación y la estabilidad de las interfaces: depuración reproducible, límites claros del enfoque y criterios para cuándo el esfuerzo realmente merece la pena.

Por qué los Streams se comportan diferente al parsear JSON en Delphi

Mientras un documento JSON sea pequeño, la ruta «leer el stream en una cadena y luego parsear» es conveniente. A partir de cierto tamaño de payload (típico: listas grandes, informes, datos de sincronización, exportaciones de logs) esto se vuelve costoso:

  • Duplicados de memoria: Lees bytes en un búfer, los conviertes a una cadena Unicode (String de Delphi = UTF-16) y el parser crea estructuras adicionales internamente. Eso puede implicar varias copias temporales.
  • Presión sobre GC/Heap: Muchas cadenas temporales y valores JSON aumentan la fragmentación y la sobrecarga de asignación, especialmente en procesos de larga duración (servicios, workers, tareas de importación).
  • El patrón de error se vuelve confuso: Si al leer ya se han asumido codificaciones incorrectas, el parser JSON solo ve «caracteres raros» o bytes de control inesperados.

Es importante una separación clara: JSON es formalmente Unicode; en el transporte es casi siempre UTF-8. Delphi trabaja internamente, sin embargo, con UTF-16. La transición de bytes (Stream) a caracteres (String) es el punto donde surgen los problemas con caracteres especiales —no el JSON en sí.

System.JSON: lo que hace bien — y dónde debes tener cuidado

System.JSON es en Delphi el estándar para JSON basado en DOM: obtienes un modelo de objetos (TJSONObject, TJSONArray), puedes consultar valores, iterar, serializar. Eso es robusto para integraciones empresariales típicas, pero tiene dos consecuencias:

  • No es un parser de streaming real: El modelo de objetos se construye por completo. Puede que te ahorres leer a una cadena adicional en algunos casos, pero el DOM sigue siendo intensivo en memoria.
  • La entrada del parser suele ser texto: Dependiendo de la versión de Delphi y de la API utilizada, pronto vuelves a trabajar con cadenas, incluida la conversión de encoding.

Si tu objetivo es un «parser de streams rápido», en la práctica normalmente significa una de dos cosas: (1) evitar copias innecesarias y (2) fail-fast lo antes posible ante payloads defectuosos. Ambas metas son alcanzables con System.JSON, siempre que controles la etapa de bytes a texto.

UTF-8-Fallstricke bei Sonderzeichen: die typischen Ursachen

Gráfico abstracto que muestra caracteres UTF-8 multibyte a través de límites de chunk y el buffering correcto en el decodificador antes del parseo JSON
El chunking es inofensivo, siempre que el decodificador UTF-8 almacene en búfer las secuencias multibyte que cruzan los límites.

Si las vocales con diéresis (ä/ö/ü/ß) u otros caracteres especiales aparecen mal en el resultado (ä, – etc.), casi siempre es un desajuste de encoding. En el entorno Delphi estas causas son especialmente frecuentes:

1) Fallback a ANSI por auxiliares «convenientes»

Algunos caminos de lectura asumen silenciosamente el encoding ANSI del sistema (Codepage del sistema Windows) cuando no se pasa una codificación explícita. Esto solo se nota cuando un payload contiene más que ASCII. En datos de prueba suele coincidir por casualidad; en producción falla con nombres reales, lugares y textos libres.

2) Confusión por BOM (Byte Order Mark)

UTF-8 puede empezar con BOM (bytes EF BB BF). En el contexto web el BOM es poco común, en archivos aparece. Algunos lectores detectan el BOM y ajustan la codificación; otros no, o solo en ciertos modos. Si un BOM entra en la cadena como un carácter normal, a menudo verás un «Zero Width No-Break Space» invisible al inicio o el parser JSON falla inmediatamente en el primer token.

3) Doble conversión (UTF-8 es interpretado «otra vez»)

El patrón clásico «ä» en lugar de «ä» ocurre cuando los bytes UTF-8 se decodificaron correctamente a Unicode pero más tarde se vuelven a interpretar erróneamente como bytes ANSI/UTF-8 (o viceversa). En Delphi esto sucede con frecuencia cuando hay conversiones poco claras entre TBytes, RawByteString y string.

4) Truncamiento en medio de un carácter multibyte

UTF-8 codifica caracteres especiales en 2–4 bytes. Si se lee en chunks (p. ej. 8 KB) y el límite del chunk cae en medio del carácter, el decodificador debe hacer buffering correctamente. Un enfoque ingenuo que convierta cada chunk por separado a cadena y las concatene produce secuencias inválidas. Esto puede parecer un error «esporádico», dependiendo de los límites de paquetes, el comportamiento de proxies o del chunking HTTP.

5) Suposiciones erróneas basadas en cabeceras HTTP

Con REST la cabecera suele ser Content-Type: application/json; charset=utf-8. Algunos servidores no incluyen charset, otros proporcionan valores incorrectos. Si usas la cabecera a ciegas, puede variar según la versión del backend. Para operaciones y soporte es útil inspeccionar el flujo real de bytes y registrarlo en caso de error.

Un enfoque limpio: Stream → Decodificador UTF-8 → JSON-Parser

La canalización robusta consta de tres etapas claras:

  1. Leer bytes del flujo (controlado, p. ej. con límite/timeout en el cliente HTTP).
  2. Decodificación a Unicode con UTF-8 explícito (tolerar BOM opcionalmente).
  3. Parseo con System.JSON a un modelo de objetos o para extracción dirigida.

La palanca más importante es la etapa 2: no quieres que en algún lugar decida el «Default Encoding». En Delphi eso significa: establecer TEncoding.UTF8 explícitamente y no confiar en conversiones implícitas.

Qué significa aquí «rápido» concretamente

Con System.JSON no vas a „eliminar“ el DOM. Pero puedes evitar:

  • una copia adicional de toda la payload como cadena intermedia, cuando internamente solo necesitas unos pocos valores (entonces conviene más otro parser; sobre eso más adelante),
  • recodificaciones múltiples,
  • y puedes leer de forma controlada payloads muy grandes (con límite de tamaño y un mensaje de error claro), en lugar de terminar en Out-of-Memory o Access Violations.

El caso límite concreto: caracteres especiales corruptos, pero solo a veces

Un caso límite de la práctica es especialmente insidioso: la payload es en principio JSON válido en UTF-8, pero la lees en chunks y conviertes cada chunk a String. Mientras solo haya ASCII, no lo notas. En cuanto un Umlaut cae exactamente en un límite de chunk, se generan secuencias UTF-8 inválidas. Resultado: caracteres corruptos o un error de parseo en una posición que no coincide con el contenido real.

Cómo reconocerlo:

  • Los errores de parseo ocurren «aleatoriamente» en respuestas grandes, no en las pequeñas.
  • La misma petición funciona a veces y a veces no (según el chunking/transport).
  • Un hexdump de los bytes muestra UTF-8 válido, pero el String registrado contiene caracteres de reemplazo (�) o el clásico mojibake.

La solución no es usar «más readln» o «buffers más grandes», sino emplear un decodificador que almacene en búfer correctamente las secuencias multibyte a través de los límites de chunk. Precisamente ahí es donde TStreamReader en combinación con una codificación UTF-8 puede ser útil —si lo inicializas correctamente.

Guía práctica: comprobar de forma reproducible UTF-8 en Delphi

Configuración de depuración con impresiones de volcado de bytes y material de trabajo para comprobar bytes UTF-8 y BOM en JSON-Payloads
Para una depuración limpia lo que cuenta primero es el flujo de bytes: BOM, truncamiento y secuencias inválidas se hacen rápidamente visibles.

Antes de tocar el parser, necesitas una configuración de depuración que te muestre la secuencia real de bytes. Para soporte y operación es oro, porque luego podrás decir con claridad si la contraparte entrega datos incorrectos o si tu pipeline los decodifica mal.

1) Comprobar los primeros bytes (BOM, inicio del JSON)

Si el JSON viene con BOM, verás en el inicio del stream EF BB BF. Justo después debería aparecer típicamente „{“ o „[„. Si ya aparece „“ en el String, el BOM no fue tratado como BOM, sino decodificado como texto.

2) Registrar los bytes en bruto en hexadecimal — pero limitado

No registres payloads completos en producción (protección de datos, costes, volumen de logs). Han demostrado ser útiles:

  • Prefijo (p. ej., primeros 256 o 1024 bytes),
  • Sufijo (últimos 256 bytes),
  • y un hash (SHA-256) para correlación, si necesitas comparar payloads.

Con ello puedes diagnosticar problemas con caracteres especiales a menudo en minutos: ¿La secuencia de bytes para „ä“ es correcta (C3 A4)? ¿Hay truncamiento? ¿Aparece un 0x00 inesperado (nullbyte), p. ej. por asumir UTF-16?

3) Registrar Content-Type y charset

En HTTP/REST: registra el Content-Type y el charset declarado. Si los bytes son claramente UTF-8 pero el charset afirma otra cosa, no debes seguirlo a ciegas en el cliente. Para JSON, UTF-8 es el estándar de facto. En caso de duda: el análisis de bytes vence al encabezado.

Parser de flujo rápido con System.JSON: diseño sin copias innecesarias

Representación esquemática de una canalización de Stream, decodificación UTF-8, límite de tamaño y parsing del DOM JSON en Delphi
Una canalización clara con límite y UTF-8 explícito separa limpiamente transporte, decodificación y análisis.

Un patrón práctico es: lees del Stream a un búfer de bytes, construyes a partir de ello un String exactamente una vez con UTF-8, y pasas ese String al parser JSON. Esto no es „streaming“ en el sentido de SAX, pero es una canalización controlada y eficiente sin cambios de codificación inesperados.

Importante para la arquitectura: diseña la función de modo que decida de forma centralizada en un solo lugar:

  • Qué regla de codificación aplica (por lo general UTF-8, BOM opcional).
  • Cuál es el tamaño máximo permitido para la payload (protección contra DoS, límite operativo).
  • Cómo son los mensajes de error (con contexto, pero sin fuga de datos).

Estrategia de lectura: buffer limitado en lugar de „StreamToString“ sin límite

Si aceptas JSON de fuentes externas (socios, clientes móviles, terceros), un límite de tamaño es obligatorio. Sin límite, una única petición desafortunada puede llevar un servicio a presión de memoria. En la práctica esto significa: al leer, contar la suma de los bytes y abortar cuando se alcance un umbral — con una excepción clara que sea comprensible en la monitorización.

Por qué a menudo espero UTF-8 „sin BOM“, pero toleraría el BOM

En payloads de REST el BOM aparece raramente. En archivos (exportaciones, edición manual) ocurre con más frecuencia. Para rutas de importación robustas es sensato tolerar el BOM, pero dejarlo visible en el log, ya que puede indicar un contexto de „mundo de archivos“ en lugar de „mundo API“.

Los asesinos silenciosos: valores por defecto de TStreamReader y lectores de texto en uso mixto

TStreamReader es cómodo, pero tienes que tener claramente controladas dos cosas:

  • Establecer el encoding de forma explícita: No confiar en que lo „detecte“.
  • Entender el buffering: El reader hace buffering internamente. Si más adelante lees el mismo Stream en otro lugar, la posición importa. Suena trivial, pero en pipelines de importación más grandes se convierte rápidamente en una fuente de errores.

Particularmente problemático es el uso mixto: primero leer una parte como bytes (por ejemplo para logging o magic bytes) y luego continuar con TStreamReader. Si no retrocedes correctamente o no inicializas el reader en la posición adecuada, leerás desde el byte 257 en lugar de desde 0. El parser JSON entonces reportará „Invalid character at position …“, aunque la payload en sí sea correcta.

Cuando los caracteres especiales siguen „rotos“ a pesar de UTF-8: escaping vs. Unicode real

JSON puede contener caracteres especiales de dos maneras:

  • Como caracteres UTF-8 reales (p. ej. „München“ como bytes C3 BC …).
  • Como secuencia de escape (p. ej. „Mu00fcnchen“).
  • Ambos son válidos. En la práctica es importante: las secuencias de escape evitan muchos problemas de transporte, pero solo enmascaran errores de codificación. Si tu sistema interpreta bytes incorrectamente en algún punto, eso es un riesgo operativo, no solo un bug cosmético. Además, las secuencias de escape pueden generar confusión en logging/monitoring cuando los equipos esperan ver “texto legible”.

    System.JSON te devuelve en ambos casos al final cadenas Delphi normales (UTF-16), siempre que el camino hasta allí haya sido correcto.

    Evaluar el rendimiento de forma realista: costes del DOM, arrays grandes y selectividad

    El mayor factor de mejora de rendimiento suele no ser “hacer el parser más rápido”, sino analizar menos. Con System.JSON eso es difícil, porque obtienes el DOM. Tres situaciones típicas:

    • Arrays grandes (10.000+ elementos): la construcción del DOM consume tiempo y RAM. Si solo necesitas 2 campos por elemento, un analizador con capacidad de streaming (SAX/Tokenizer) suele ser más adecuado. System.JSON no está diseñado para eso.
    • Objetos individuales con muchos campos: si solo necesitas pocos campos, aún puedes usar el DOM, pero evita recorrerlo varias veces. Recupera los valores una sola vez y mapea esos valores en tus estructuras.
    • Varios payloads grandes consecutivos: en tareas de importación o en sync-workers conviene encapsular el parseo en un paso claro y liberar todas las referencias tras cada documento para que el gestor de memoria pueda limpiar. Es trivial, pero en servicios suele hacerse ‚de manera colateral‘.

    Un “analizador de stream” rápido con System.JSON suele ser un buen compromiso: leer de forma eficiente y correcta, usar el DOM de forma consciente y definir límites. Si necesitas semántica de streaming real (p. ej. procesar elementos de un array uno a uno sin mantenerlos todos en memoria), System.JSON no es la base adecuada.

    Robustez en producción: patrones de error y cómo hacerlos inmediatamente identificables

    Los errores de parseo JSON en los logs a menudo no son útiles porque solo indican una posición. Para operación y soporte necesitas contexto:

    • Posición en bytes vs. posición en caracteres: en UTF-8 no son idénticas. Si el parser informa una posición en caracteres, la posición en bytes puede diferir. Para volcados de bytes la posición en bytes es la que importa.
    • Fragmento alrededor del punto del error: registra en el log, en caso de error, una pequeña ventana alrededor de la posición (p. ej. 40 caracteres antes/después), pero solo si no contiene datos sensibles. Alternativamente: registra solo en hexadecimal.
    • Correlación: Request-ID, Endpoint, Partner-ID, hash del payload. Si no, no volverás a encontrar “ese error”.

    El objetivo es que, tras un incidente en producción, puedas responder en pocos minutos: “codificación interpretada incorrectamente”, “payload truncado”, “el servidor devuelve JSON inválido” o “tenemos un problema de mapeo”.

    Evitar trampas de UTF-8 de forma deliberada: lista de comprobación

    • Codificación siempre explícita: al leer desde un stream y al escribir en logs/archivos no confíes en los valores por defecto.
    • No convertir chunks a string: si lees por chunks, acumula los bytes o utiliza un decodificador que haga buffering de secuencias multibyte.
    • Tolerante con BOM, pero visible: aceptarlo, pero poder detectarlo durante la depuración.
    • Establecer límites: tamaño máximo de payload, profundidad máxima de objeto/array (si puedes controlarla), timeouts en el cliente HTTP.
  • Responsabilidades separadas: „lectura del transporte“ y „parseo de JSON“ encapsular por separado. De ese modo se depura más rápido y será posible sustituir el parser más adelante.
  • ¿Cuándo merece la pena realmente el esfuerzo para un stream-parser?

    No necesitas optimizar cada punto donde aparece JSON. Este enfoque suele merecer la pena típicamente cuando se cumple al menos una de las siguientes condiciones:

    • Payloads grandes (varios MB) aparecen con regularidad o pueden aparecer.
    • Procesos de larga ejecución (Service, Worker) procesan muchos payloads y observas picos de memoria o fragmentación.
    • Interoperabilidad con sistemas heterogéneos: varios socios, plataformas diferentes, codificaciones ocasionalmente erróneas.
    • Historial de incidentes: ya hubo diacríticos (Umlaute) dañados, errores de parseo esporádicos o interrupciones de importación difíciles de reproducir.

    Si tus payloads son pequeños y provienen de una fuente controlada, a menudo basta una solución simple; pero incluso entonces: configurar UTF-8 explícitamente cuesta casi nada y evita sorpresas posteriores.

    Delimitación: cuando necesitas streaming real

    System.JSON está orientado al DOM. Si quieres procesar los datos realmente “en el paso”, p. ej. un array grande elemento por elemento sin mantenerlo completo, necesitas otro enfoque de parser (Tokenizer/SAX). Esto no es un juicio de valor, sino una decisión de arquitectura:

    • DOM (System.JSON): cómodo, adecuado para objetos de negocio típicos, pero intensivo en memoria.
    • Streaming/SAX: menor consumo de memoria, adecuado para datos muy grandes, pero requiere más esfuerzo de implementación y un manejo de errores más cuidadoso.

    Un compromiso razonable suele ser: construir el manejo de streams, la codificación y los límites de forma limpia, y solo entonces decidir si el DOM aún encaja. En muchos proyectos, eso ya estabiliza el funcionamiento de forma notable.

    Conclusión: JSON en Delphi será fiable si tratas la codificación y los streams como una capa propia

    La mayoría de los problemas relacionados con „JSON en Delphi“ no tienen que ver con el parser JSON en sí, sino con el tramo discreto anterior: los bytes de un stream se convierten en texto. Si allí tratas UTF-8 explícitamente, detectas los casos de BOM, no ignoras los límites de chunk y estableces límites claros de tamaño, desaparecen los errores típicos con caracteres especiales —y los problemas de parseo esporádicos pasan a ser reproducibles.

    System.JSON sigue siendo un estándar pragmático: no es el parseador en streaming más rápido, pero es sólido si controlas la entrada y aceptas conscientemente los costes del DOM. Si quieres, podemos revisar juntos tu ruta de importación/REST concreta y localizar el punto en el que la codificación o el chunking falla: ponte en contacto.

    Para este tema también son importantes los parseadores de flujo JSON. El artículo sitúa estos aspectos de forma comprensible y muestra en qué hay que fijarse en la práctica.

    Discutir un proyecto o una iniciativa de modernización con Net-Base.

    siguiente paso

    Cuando un tema se convierte en un proyecto real, arquitectura, entorno existente y operación deben considerarse conjuntamente desde el inicio.

    No solo apoyamos en consultas puntuales, sino también cuando, a partir de fragmentos de código fuente, temas heredados o ideas de portales, debe consolidarse un proyecto empresarial robusto.

    • La situación actual, el estado objetivo y los riesgos técnicos se evalúan conjuntamente.
    • REST, el acceso a datos, los portales y el despliegue no se relegan a fases posteriores.
    • Usted detecta con antelación qué camino es viable, tanto económica como operativamente.

    Compartir entrada

    Compartir esta publicación directamente

    LinkedIn, X, XING, Facebook, WhatsApp y correo electrónico están disponibles de inmediato. Para Instagram preparamos el enlace y el texto breve directamente.

    Correo electrónico

    Instagram se abre en una nueva pestaña. El enlace y el texto breve se copian previamente en el portapapeles.