JSON y XML: qué son, en qué se diferencian y cuándo usar cada uno

JSON y XML en un esquema: el mismo pedido de tutienda.com en los dos formatos y cómo se corresponden sus piezas (objeto y elemento, atributos, arreglo y elementos repetidos, el null que no aparece), 277 contra 424 bytes, qué llega al programa (JSON con tipos, XML todo texto), bien formado frente a válido, errores típicos con sus mensajes reales y cuándo elegir cada uno. Esquema: decodigo.com.

JSON y XML son las dos formas más comunes de escribir datos en texto para que un programa se los pase a otro: la respuesta de una API, la factura electrónica que te manda un proveedor, la configuración de un proyecto o un documento de Word por dentro. Los dos guardan lo mismo, datos con estructura, pero lo escriben de manera distinta y cada uno tiene sus reglas, sus errores típicos y sus usos. En esta guía escribimos un mismo pedido en los dos formatos, explicamos sus piezas, la diferencia entre un archivo bien formado y uno válido, dónde se usa cada uno hoy y cuándo conviene elegir cuál. Todos los mensajes de error que aparecen los obtuvimos ejecutando los parsers de verdad.

Qué son, en pocas palabras

JSON (JavaScript Object Notation) nació de la forma en que JavaScript escribe sus objetos, aunque hoy lo lee cualquier lenguaje. Se presentó en el sitio JSON.org en 2001 y lo describen dos normas que dicen lo mismo: el RFC 8259 del IETF y el estándar ECMA-404, ambos de diciembre de 2017. Ecma lo define como una sintaxis «de llaves, corchetes, dos puntos y comas».

XML (Extensible Markup Language) es un lenguaje de marcado: el texto va entre etiquetas con nombre, como en HTML, pero las etiquetas las inventa quien diseña el formato. Es una recomendación del W3C desde el 10 de febrero de 1998, y la versión vigente es la quinta edición de XML 1.0, de noviembre de 2008. Viene de SGML, el abuelo de HTML, y se pensó para documentos además de datos.

Una forma de verlo: JSON se parece a un formulario lleno, con casillas y valores; XML, a un expediente con carpetas etiquetadas, que puede llevar dentro tanto datos sueltos como párrafos de texto.

El mismo pedido en JSON y en XML

Este es un pedido de una tienda en línea inventada, tutienda.com, escrito en JSON:

{
  "pedido": 1042,
  "cliente": "Ana López",
  "pagado": true,
  "cupon": null,
  "articulos": [
    {"sku": "CAF-250", "nombre": "Café de Chiapas 250 g", "precio": 189.50, "cantidad": 2},
    {"sku": "TAZ-01", "nombre": "Taza de barro", "precio": 120, "cantidad": 1}
  ]
}

Y el mismo pedido en XML. El número de pedido y si está pagado van como atributos de la etiqueta <pedido>; el resto, como elementos anidados. El cupón vacío simplemente no aparece:

<?xml version="1.0" encoding="UTF-8"?>
<pedido id="1042" pagado="true">
  <cliente>Ana López</cliente>
  <articulos>
    <articulo sku="CAF-250" cantidad="2">
      <nombre>Café de Chiapas 250 g</nombre>
      <precio moneda="MXN">189.50</precio>
    </articulo>
    <articulo sku="TAZ-01" cantidad="1">
      <nombre>Taza de barro</nombre>
      <precio moneda="MXN">120</precio>
    </articulo>
  </articulos>
</pedido>

Lo mismo, escrito de dos maneras JSON XML Un registro { … } <pedido> … </pedido> Un campo "cliente": "Ana López" <cliente>Ana López</cliente> Un dato corto "pedido": 1042 <pedido id="1042"> Una lista "articulos": [ {…}, {…} ] <articulo> repetido Un número 189.50 es un número 189.50 es texto Sí o no true / false "true": texto, o el esquema Nada null se omite el elemento Ejemplos del pedido de esta guía.

Cómo se corresponden las piezas de JSON y de XML, con el pedido de esta guía. Esquema: decodigo.com.

La diferencia más importante no se ve a simple vista. Al leer el JSON con Python, cada valor llega con su tipo: el número es un entero, true es un booleano y null es «nada» (None):

$ python3 leer_json.py pedido.jsonpedido: 1042 (int)
cliente: 'Ana López' (str)
pagado: True (bool)
cupon: None (NoneType)
articulos: [{'sku': 'CAF-250', 'nombre': 'Café de Chiapas 250 g', 'precio': 189.5, 'cantidad': 2}, {'sku': 'TAZ-01', 'nombre': 'Taza de barro', 'precio': 120, 'cantidad': 1}] (list)

Al leer el XML con xml.etree.ElementTree, la biblioteca estándar de Python, todo llega como texto: '1042', 'true' y '189.50' son cadenas, y convertirlas es trabajo tuyo o de un esquema:

$ python3 leer_xml.py pedido.xmlraíz: pedido {'id': '1042', 'pagado': 'true'}
  cliente {} 'Ana López'
  articulos {}
  articulo {'sku': 'CAF-250', 'cantidad': '2'}
  nombre {} 'Café de Chiapas 250 g'
  precio {'moneda': 'MXN'} '189.50'
  articulo {'sku': 'TAZ-01', 'cantidad': '1'}
  nombre {} 'Taza de barro'
  precio {'moneda': 'MXN'} '120'

A cambio, XML conserva cosas que JSON no tiene, como el atributo moneda="MXN" pegado al precio. Y el XML ocupa más: 424 bytes contra 277 del JSON tal como están escritos arriba, y 366 contra 229 quitando los espacios, porque cada etiqueta se escribe dos veces, al abrir y al cerrar.

JSON por dentro: seis clases de valores

JSON tiene muy pocas piezas. El RFC 8259 define cuatro tipos simples y dos compuestos:

  • Objetos, entre llaves: pares de nombre y valor separados por comas, {"cliente": "Ana López"}. El nombre siempre es una cadena con comillas dobles.
  • Arreglos, entre corchetes: una lista ordenada de valores, [1, 2, 3], que pueden ser de distintos tipos.
  • Cadenas, siempre entre comillas dobles, con escapes como \n o \u00e9.
  • Números, en decimal, con o sin fracción y exponente: 1042, 189.50, 1e-3. No hay NaN ni Infinity: el RFC los prohíbe expresamente.
  • Booleanos: true y false, en minúsculas.
  • null, también en minúsculas, para «sin valor».

Y ya. No hay fechas (se mandan como texto, por lo general en formato ISO 8601, "2026-11-09"), no hay comentarios y no hay datos binarios: ECMA-404 avisa que JSON no está pensado para ellos, y lo habitual es mandarlos codificados en Base64. Tres reglas más del RFC que conviene saber:

  • UTF-8 obligatorio entre sistemas que no sean un entorno cerrado, y sin la marca de orden de bytes (BOM) al principio.
  • Nombres repetidos: los nombres de un objeto «deberían» ser únicos. Si no lo son, el RFC dice que el resultado es impredecible y que muchas implementaciones se quedan con el último valor.
  • Números grandes: solo los enteros entre −(253−1) y 253−1 son seguros en todas partes, porque muchos lenguajes guardan los números de JSON como de doble precisión. Por eso los identificadores muy largos se suelen mandar como cadenas.

Lo comprobamos con Python, jq y Node.js. Con un nombre repetido, los tres se quedaron con el último valor sin avisar:

{"precio": 100, "precio": 120}
el mismo archivo en los trespython3: precio: 120 (int)
jq:      {"precio":120}
node:    {"precio":120}

Y con el número 9007199254740993, que es 253+1, Python y jq 1.7 lo conservaron, pero Node.js, como cualquier navegador, lo convirtió en otro número sin dar ningún error:

{"id": 9007199254740993}python3: id: 9007199254740993 (int)
jq:      {"id":9007199254740993}
node:    {"id":9007199254740992}

El tipo de medio de JSON en HTTP es application/json, el que verás en la cabecera Content-Type de casi cualquier API; cómo viajan esas peticiones lo explicamos en los métodos HTTP.

XML por dentro: elementos, atributos y compañía

XML tiene más piezas, porque además de datos sirve para documentos:

  • Elementos: una etiqueta de apertura, su contenido y una de cierre, <cliente>Ana López</cliente>. Si no tienen contenido se pueden cerrar en la misma etiqueta: <pedido id="1042"/>. Los nombres distinguen mayúsculas de minúsculas, y todo documento tiene un solo elemento raíz que envuelve a los demás.
  • Atributos: pares de nombre y valor dentro de la etiqueta de apertura, siempre entre comillas, simples o dobles: sku="CAF-250". Un elemento no puede repetir un atributo.
  • La declaración: la primera línea, <?xml version="1.0" encoding="UTF-8"?>, dice la versión y la codificación. La norma recomienda ponerla, pero un documento sin ella también es correcto.
  • Entidades: los caracteres < y & no pueden ir sueltos en el texto, porque marcan el inicio de una etiqueta o de una entidad. Se escriben con una de las cinco entidades predefinidas: &lt;, &gt;, &amp;, &apos; y &quot;, o con una referencia numérica como &#60;.
  • Secciones CDATA: un bloque <![CDATA[ … ]]> donde el texto se toma tal cual, sin escapar nada. Sirve para meter código o texto con muchos símbolos.
  • Comentarios: <!-- así -->. Dentro no puede ir la secuencia --.
  • Espacios de nombres (namespaces): para mezclar en un mismo documento etiquetas de vocabularios distintos sin que choquen, cada vocabulario se asocia a un identificador único, casi siempre con forma de URL, que no tiene por qué existir como página. Los define otra recomendación del W3C, Namespaces in XML.

Así se ve un CDATA y lo que entrega el parser: el texto, con sus < y && intactos:

<nota><![CDATA[Si x < 10 && y > 2, aplica el descuento]]></nota>
$ python3 leer_xml.py cdata.xmlraíz: nota {} 'Si x < 10 && y > 2, aplica el descuento'

Y así un documento con dos espacios de nombres, uno para el pedido y otro, con el prefijo env:, para los datos del envío. El parser guarda cada etiqueta con su espacio de nombres completo entre llaves:

<?xml version="1.0" encoding="UTF-8"?>
<pedido xmlns="urn:tutienda:pedidos" xmlns:env="urn:tutienda:envios" id="1042">
  <cliente>Ana López</cliente>
  <env:direccion>
    <env:cp>06600</env:cp>
  </env:direccion>
</pedido>
$ python3 leer_xml.py espacios.xmlraíz: {urn:tutienda:pedidos}pedido {'id': '1042'}
  {urn:tutienda:pedidos}cliente {} 'Ana López'
  {urn:tutienda:envios}direccion {}
  {urn:tutienda:envios}cp {} '06600'

Esto es justo lo que hace la factura electrónica mexicana, como veremos abajo: las etiquetas del SAT llevan el prefijo cfdi: y el timbre fiscal, otro distinto.

Bien formado y válido: los esquemas

En los dos formatos hay dos preguntas distintas que se confunden a menudo:

Bien formado no es lo mismo que válido El texto que llega 1. ¿Está bien formado? Lo revisa el parser, siempre Sintaxis: comillas, comas, llaves, etiquetas cerradas, & escapado Falla: <nombre>Pan & café</nombre> 2. ¿Es válido? Solo si tienes un esquema Contenido: campos obligatorios, tipos, valores permitidos Falla: pagado = sí (no es booleano) La aplicación usa los datos Esquemas: JSON Schema para JSON; XSD o DTD para XML.

Las dos comprobaciones de un documento JSON o XML. Los dos fallos de ejemplo son del laboratorio de esta guía. Esquema: decodigo.com.
  • ¿Está bien formado? Es decir, ¿respeta la sintaxis? Lo comprueba siempre el parser al leer. En XML la norma es tajante: un error de buena formación es un «error fatal», y el programa que lee el archivo no debe seguir entregando datos como si nada.
  • ¿Es válido? Es decir, ¿trae lo que espera tu aplicación: los campos obligatorios, con sus tipos y valores permitidos? Para eso hace falta un esquema, un segundo documento que describe la forma de los datos. En XML hay dos clásicos: la DTD, que viene de la propia norma XML 1.0, y el XSD (XML Schema), también del W3C y mucho más preciso con los tipos. En JSON, el más usado es JSON Schema, cuya versión actual es la 2020-12.

Para probarlo escribimos un esquema XSD para el pedido y un JSON Schema equivalente, y dos pedidos con errores de contenido: sin cliente, con pagado igual a «sí» y con un precio escrito con letra o entre comillas. Los dos archivos están bien formados:

<?xml version="1.0" encoding="UTF-8"?>
<pedido id="1042" pagado="sí">
  <articulos>
    <articulo sku="CAF-250" cantidad="2">
      <nombre>Café de Chiapas 250 g</nombre>
      <precio moneda="MXN">ciento ochenta</precio>
    </articulo>
  </articulos>
</pedido>
$ xmllint --noout pedido-invalido.xmlbien formado: sin errores

Pero el esquema los rechaza. xmllint, la herramienta de línea de comandos de libxml2, se detiene en el primer problema de cada elemento y explica qué esperaba:

$ xmllint --noout --schema pedido.xsd pedido-invalido.xmlpedido-invalido.xml:2: element pedido: Schemas validity error : Element 'pedido', attribute 'pagado': 'sí' is not a valid value of the atomic type 'xs:boolean'.
pedido-invalido.xml:3: element articulos: Schemas validity error : Element 'articulos': This element is not expected. Expected is ( cliente ).
pedido-invalido.xml fails to validate

Con JSON pasa lo mismo. jq lo lee sin protestar, porque la sintaxis es correcta, y la biblioteca jsonschema de Python encuentra los tres errores:

{
  "pedido": 1042,
  "pagado": "sí",
  "articulos": [
    {"sku": "CAF-250", "precio": "189.50", "cantidad": 2}
  ]
}
$ python3 validar_json.py pedido.schema.json pedido-invalido.json(raíz) → 'cliente' is a required property
articulos/0/precio → '189.50' is not of type 'number'
pagado → 'sí' is not of type 'boolean'
3 errores

Validar contra un esquema es lo que te ahorra escribir a mano decenas de if para revisar cada campo que llega de fuera. Si quieres generar las clases o interfaces que corresponden a un JSON en tu lenguaje, prueba JSON a tipos. Y para revisar si un XML está bien formado sin instalar nada, usa nuestro validador y formateador de XML.

Dónde se usa cada uno hoy

JSON lo encontrarás sobre todo en lo que habla con un navegador o con una app:

  • APIs REST y servicios web: la petición y la respuesta van en JSON con Content-Type: application/json.
  • Configuración de proyectos: el package.json de npm, que según su documentación «debe ser JSON de verdad, no solo un objeto literal de JavaScript», y el composer.json de PHP, entre muchos otros.
  • Bases de datos: PostgreSQL, MySQL y SQLite guardan y consultan columnas JSON, como mostramos en JSON en SQL.
  • Tokens y registros: el contenido de un JWT es JSON (puedes verlo con nuestro decodificador de JWT), y muchos sistemas escriben sus logs como una línea JSON por evento.

XML sigue en muchos lugares, a veces sin que se note:

  • La factura electrónica en México: el CFDI 4.0 es un XML que sigue el esquema que publica el SAT dentro del Anexo 20. Su elemento raíz es cfdi:Comprobante, con el espacio de nombres http://www.sat.gob.mx/cfd/4 y el atributo Version="4.0", y dentro van Emisor, Receptor, Conceptos, Impuestos y el Complemento con el Timbre Fiscal Digital, que tiene su propio espacio de nombres y su versión 1.1. Si te llega uno, revísalo con nuestro validador de CFDI.
  • Documentos de Office: un .docx o un .xlsx es en realidad un archivo ZIP lleno de archivos XML (formato Office Open XML, normas ECMA-376 e ISO/IEC 29500). Si le cambias la extensión a .zip y lo abres, verás xl/workbook.xml, xl/worksheets/sheet1.xml y compañía.
  • RSS: los feeds de noticias y de pódcasts. La especificación de RSS 2.0 lo llama «un dialecto de XML».
  • SVG: las gráficas vectoriales, como los esquemas de esta página. Un archivo .svg suelto usa la sintaxis XML; dentro de una página HTML, la de HTML.
  • Android y Java: todo proyecto de Android lleva un AndroidManifest.xml, y las vistas clásicas se describen en XML. Maven describe cada proyecto de Java en un pom.xml.
  • Sistemas empresariales y bancarios: los servicios web SOAP y muchos intercambios entre empresas y gobiernos siguen funcionando con XML y esquemas XSD.

Errores típicos y lo que dice cada parser

Estos son los errores que más se repiten, con lo que respondieron Python 3.13, jq 1.7 y Node.js 20 (el mismo motor de JavaScript que Chrome) para JSON, y Python y xmllint para XML. Conocer los mensajes ayuda a reconocerlos al vuelo.

JSON: la coma de más al final

Muchos lenguajes permiten una coma después del último elemento; JSON no:

{
  "pedido": 1042,
  "pagado": true,
}
python3 · jq · nodejson.decoder.JSONDecodeError: Illegal trailing comma before end of object: line 3 column 17 (char 36)
jq: parse error: Expected another key-value pair at line 4, column 1
SyntaxError: Expected double-quoted property name in JSON at position 38

Fíjate en que cada uno señala un lugar distinto: Python marca la coma misma, y jq y Node.js, el carácter en el que se dieron cuenta de que faltaba algo, la llave de cierre. Es normal que el error aparezca una línea más abajo de donde está el problema.

JSON: comillas simples

En JavaScript y en Python, 'pedido' es una cadena válida. En JSON, las cadenas y los nombres van siempre entre comillas dobles:

{
  'pedido': 1042
}
python3 · jq · nodejson.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 2 column 3 (char 4)
jq: parse error: Invalid numeric literal at line 2, column 11
SyntaxError: Expected property name or '}' in JSON at position 4

El mensaje de jq despista: «Invalid numeric literal» no tiene nada que ver con números; es lo que dice cuando encuentra una palabra que no sabe leer.

JSON: comentarios

La gramática de JSON no tiene comentarios, así que un // rompe el archivo:

{
  // número de pedido
  "pedido": 1042
}
python3 · jq · nodejson.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 2 column 3 (char 4)
jq: parse error: Invalid numeric literal at line 2, column 5
SyntaxError: Expected property name or '}' in JSON at position 4

Algunas herramientas aceptan variantes con comentarios: VS Code, por ejemplo, lee su settings.json y su launch.json en un modo que llama «JSON with Comments». Pero eso es una extensión de esas herramientas, no JSON. Si necesitas comentar una configuración, YAML y TOML sí los permiten.

JSON: NaN, el caso en que no todos están de acuerdo

El RFC prohíbe NaN, pero no todos los parsers lo rechazan. Python lo aceptó y lo convirtió en un número «no es un número»; jq lo aceptó y lo escribió como null; solo Node.js dio error:

{"precio": NaN}python3: precio: nan (float)
jq:      {"precio":null}
node:    SyntaxError: Unexpected token 'N', "{"precio": NaN} " is not valid JSON

La lección: que tu programa lea un JSON no significa que sea correcto. Si lo vas a mandar a otro sistema, valídalo con un parser estricto, como el de nuestro formateador y validador de JSON, que te dice en qué línea está el error.

XML: una etiqueta sin cerrar

<pedido id="1042">
  <cliente>Ana López
</pedido>
python3 · xmllintxml.etree.ElementTree.ParseError: mismatched tag: line 3, column 2

sin-cerrar.xml:3: parser error : Opening and ending tag mismatch: cliente line 2 and pedido
</pedido>
         ^
sin-cerrar.xml:4: parser error : Premature end of data in tag pedido line 1

^

xmllint es más claro: dice que el <cliente> de la línea 2 se quiso cerrar con </pedido>. Lo mismo pasa cuando la etiqueta de cierre no coincide en mayúsculas y minúsculas, porque para XML Cliente y cliente son nombres distintos:

<Cliente>Ana López</cliente>xml.etree.ElementTree.ParseError: mismatched tag: line 2, column 22

mayusculas.xml:2: parser error : Opening and ending tag mismatch: Cliente line 2 and cliente
  <Cliente>Ana López</cliente>
                               ^

XML: un & sin escapar

El error más común al generar XML pegando cadenas a mano: un nombre de producto, una razón social o una URL con &.

<articulo>
  <nombre>Pan & café</nombre>
</articulo>
python3 · xmllintxml.etree.ElementTree.ParseError: not well-formed (invalid token): line 2, column 15

ampersand.xml:2: parser error : xmlParseEntityRef: no name
  <nombre>Pan & café</nombre>
               ^

Se arregla escribiendo Pan &amp; café, o mejor, dejando que una biblioteca genere el XML y escape el texto por ti: así lo hacen todos los ejemplos de crear XML que enlazamos abajo.

XML: dos raíces

Juntar dos documentos en un archivo no da un documento más grande, sino uno roto, porque solo puede haber un elemento raíz:

python3 · xmllintxml.etree.ElementTree.ParseError: junk after document element: line 2, column 0

dos-raices.xml:2: parser error : Extra content at the end of the document
<pedido id="1043"/>
^

La solución es envolverlos en un elemento común, por ejemplo <pedidos>. Todos estos errores los puedes localizar pegando tu XML en el validador de XML.

JSON o XML: cuándo elegir cada uno

JSON o XML: cuándo elegir cada uno Elige JSON si… hablas con un navegador o una app es una API REST o un archivo de config quieres números, true y null como tipos buscas lo más corto y sencillo Elige XML si… el documento mezcla texto y marcado necesitas atributos y espacios de nombres el contrato es un esquema XSD estricto hay firmas, XSLT o XPath de por medio No lo eliges tú CFDI del SAT, .docx, .xlsx, SVG, RSS, pom.xml y Android: XML package.json, composer.json y el contenido de un JWT: JSON

Cuándo conviene cada formato, y los casos en que el formato ya viene decidido. Esquema: decodigo.com.

Para la mayoría de los proyectos nuevos la respuesta corta es JSON: es más corto, se lee casi igual en todos los lenguajes, los navegadores lo entienden sin bibliotecas extra y sus tipos (números, booleanos, null) evitan conversiones. Es la elección natural para una API, para la comunicación entre el navegador y el servidor y para la configuración de un programa.

XML gana cuando lo que guardas se parece más a un documento que a una ficha: texto con partes marcadas dentro, como un libro o una página, metadatos en atributos, vocabularios mezclados con espacios de nombres o un contrato muy estricto entre empresas descrito en XSD. También tiene un ecosistema maduro para consultar (XPath), transformar (XSLT) y firmar documentos, y es el formato que eligió el SAT para la factura electrónica.

Y muchas veces no hay decisión que tomar: si trabajas con el CFDI, con archivos de Office, con un feed RSS o con un proyecto de Maven, el formato es XML porque así lo define quien lo creó. Lo mismo al revés con package.json o con una API que solo responde JSON.

Leer XML sin riesgos

JSON es solo datos: no tiene forma de pedirle al parser que haga nada. Su único riesgo clásico es leerlo con eval(), que ejecutaría cualquier código metido en el texto; el RFC 8259 lo desaconseja expresamente, y por eso se usa siempre JSON.parse o la función equivalente de tu lenguaje.

XML es distinto, porque la DTD permite definir entidades, una especie de abreviaturas que el parser sustituye por su contenido. En este documento, &tienda; es una abreviatura inofensiva de «tutienda.com», y la biblioteca estándar de Python la expande:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE pedido [
  <!ENTITY tienda "tutienda.com">
]>
<pedido id="1042">
  <origen>&tienda;</origen>
</pedido>
$ python3 leer_xml.py entidad-interna.xmlraíz: pedido {'id': '1042'}
  origen {} 'tutienda.com'

Ese mecanismo es la base de dos ataques muy conocidos contra los programas que leen XML que llega de fuera:

  • XXE (XML External Entity): una entidad que, en lugar de un texto, apunta a un archivo del servidor o a una dirección de red. Si el parser la resuelve, puede acabar leyendo archivos internos o conectándose a otras máquinas en nombre del atacante.
  • «Billion laughs» (mil millones de risas): entidades que se refieren unas a otras varias veces en cadena, de modo que unos cuantos cientos de bytes se convierten, al expandirse, en gigabytes de texto. La memoria y el procesador se agotan y el servicio se cae.

La defensa es la misma idea en todos los lenguajes: si no necesitas DTD ni entidades, apágalas. Muchas bibliotecas modernas ya vienen así de fábrica:

  • Python: la documentación oficial explica que Expat, el parser que usa la biblioteca estándar, no abre archivos ni conexiones por su cuenta, y que las versiones anteriores a la 2.7.2 pueden ser vulnerables a «billion laughs» (lo puedes revisar en pyexpat.EXPAT_VERSION). Para XML que no controlas, recomienda la biblioteca defusedxml, que rechaza cualquier declaración de entidad. Con lxml, crea el parser con resolve_entities=False y no_network=True.
  • Java: en DocumentBuilderFactory y SAXParserFactory, activa la función http://apache.org/xml/features/disallow-doctype-decl y XMLConstants.FEATURE_SECURE_PROCESSING, como recomienda la guía de OWASP.
  • C# y .NET: XmlReader prohíbe las DTD por omisión (DtdProcessing.Prohibit) y, desde .NET Framework 4.5.2, no resuelve recursos externos. No cambies esos valores para datos de fuera.
  • PHP: desde libxml 2.9.0 la sustitución de entidades viene apagada; no actives LIBXML_NOENT ni LIBXML_DTDLOAD con XML que no controlas. La vieja función libxml_disable_entity_loader está obsoleta desde PHP 8.0 precisamente por eso.

Con el mismo archivo inofensivo de arriba, así responden las dos opciones defensivas de Python: defusedxml se niega a leerlo y lxml, configurado como dijimos, lo lee sin sustituir la entidad:

$ python3 defensa_xml.py entidad-interna.xmldefusedxml.EntitiesForbidden: EntitiesForbidden(name='tienda', system_id=None, public_id=None)
$ python3 defensa_lxml.py entidad-interna.xml<pedido id="1042">
  <origen>&tienda;</origen>
</pedido>

La regla general: trata cualquier XML que venga de un usuario, de un proveedor o de internet como no confiable, y revisa cómo viene configurado tu parser antes de ponerlo en producción.

¿Y YAML o TOML?

Son los otros dos formatos de texto que verás a menudo, sobre todo en archivos de configuración. YAML usa la sangría en lugar de llaves y permite comentarios; lo encontrarás en Docker Compose, Kubernetes y GitHub Actions. TOML se parece a los viejos archivos .ini y lo usan, entre otros, el Cargo.toml de Rust y el pyproject.toml de Python. Los dos se pueden convertir a JSON y de vuelta, y para eso tenemos el convertidor de JSON, YAML, TOML y CSV.

Ejemplos en tu lenguaje

Leer y escribir estos formatos es algo que harás en casi cualquier lenguaje. Estos son nuestros ejemplos, todos probados:

JSON

En Go hay novedades: encoding/json/v2 ya es estable en Go 1.27.

XML

Herramientas para practicar

Fuentes: RFC 8259, The JavaScript Object Notation (JSON) Data Interchange Format (IETF, diciembre de 2017); ECMA-404, The JSON Data Interchange Syntax (2.ª edición, diciembre de 2017); Extensible Markup Language (XML) 1.0, quinta edición (W3C, 26 de noviembre de 2008) y la primera edición (10 de febrero de 1998); Namespaces in XML 1.0 (W3C, 2009); especificación de JSON Schema; el esquema del CFDI 4.0 que publica el SAT (cfdv40.xsd); la documentación de npm, RSS 2.0, SVG 2, Android, Maven y ECMA-376; XML security en la documentación de Python; OWASP XML External Entity Prevention Cheat Sheet; manual de PHP. Consultados el 9 de noviembre de 2026. Los mensajes de error se obtuvieron ese día en un contenedor desechable sin red con Debian 13, Python 3.13.5, jq 1.7.1, libxml2 2.9.14, Node.js 20.19.2, lxml 5.4.0, jsonschema 4.19.2 y defusedxml 0.7.1.