InicioNotas › SIFEN

Error 0160 de SIFEN: «XML mal formado» cuando el XML está perfecto

Por Javier Sempere · Agentiko ·

Respuesta corta: el 0160 significa que tu XML no valida contra el XSD de la SET. Tu editor te dice que está bien formado y es verdad — «bien formado» y «válido contra el esquema» son cosas distintas.

La causa que a nadie se le ocurre: un salto de línea dentro de un texto libre. Los tipos string del XSD llevan el patrón .*[^\s].*, y en XML Schema el punto no casa saltos de línea. Un \n en la descripción de un ítem tumba el documento entero. No es un problema de longitud.

Por qué duele tanto

La SET devuelve un solo error bloqueante por intento. Arreglás uno y aparece el siguiente. Eso convierte la integración en una escalera a ciegas, y hace que valga oro saber de antemano cuáles son los peldaños.

El 0160 es el primero de todos, porque la validación de esquema va antes que cualquier regla de negocio. Mientras no lo pases, la SET no te va a contar nada más.

La causa que nadie espera: un salto de línea

En el XSD de la SET, los campos de texto libre no son string a secas. Heredan de un tipo llamado noEmptyString, y esta es su definición completa, tal cual está en DE_Types_v150.xsd:

<xs:simpleType name="noEmptyString">
  <xs:annotation>
    <xs:documentation>Cadena no vacia</xs:documentation>
  </xs:annotation>
  <xs:restriction base="xs:string">
    <xs:whiteSpace value="preserve" />
    <xs:pattern value=".*[^\s].*" />
    <xs:minLength value="1" />
  </xs:restriction>
</xs:simpleType>

Las dos líneas resaltadas son el mecanismo completo del rechazo, y hay que leerlas juntas:

  • whiteSpace="preserve" le dice al validador que no toque el espacio en blanco. Si fuera collapse, el salto de línea se convertiría en un espacio antes de comparar y no habría problema. Al preservarlo, el \n llega vivo hasta el patrón.
  • .*[^\s].* quiere decir «que haya al menos un carácter que no sea espacio». La intención es razonable: impedir campos llenos de espacios. Pero en XML Schema el punto no casa saltos de línea, y no existe un modificador «dotall» que se pueda activar. Así que en cuanto el valor contiene un \n, el patrón deja de casar y el documento no valida.

Y no es un campo aislado: en la versión 150 del esquema hay 79 campos que heredan de noEmptyString — la descripción del ítem, la información adicional, el número de lote, el chasis, el color… Cualquiera de ellos con un salto de línea produce el mismo rechazo.

Y ese \n aparece solo, sin que nadie lo escriba a mano. El caso típico: un ERP que guarda el nombre del producto y su descripción en campos separados y los concatena con un salto de línea al armar la línea de la factura. En el sistema se ve perfecto. En el XML es un rechazo.

No es longitud. Es el error de diagnóstico más común acá: se asume que el texto es demasiado largo y se recorta. dDesProSer admite 2000 caracteres. Recortar no arregla nada y hace perder horas.

Cómo se arregla

Aplanando los espacios en blanco de todo texto libre justo antes de serializar. No solo la descripción del ítem: también la razón social, la dirección y cualquier observación — en todos aparece el mismo patrón en el XSD.

# Ruby, pero la idea es la misma en cualquier lenguaje:
# colapsar todo espacio en blanco (incluidos \n, \r y \t) a un espacio simple.
def texto_sifen(valor)
  valor.to_s.gsub(/\s+/, " ").strip
end

Ojo con dos atajos que no alcanzan: strip a secas solo limpia los extremos y deja el salto del medio, y reemplazar \n por "" pega las dos palabras («ProductoDescripción»). Colapsar a un espacio simple es lo que da un texto que además se lee bien en el KuDE.

Cómo se verifica

Sin mandar nada a la SET: validá el XML contra el XSD oficial en tu propia máquina. Es la diferencia entre averiguarlo en un segundo y averiguarlo en un viaje de ida y vuelta al servicio web.

# con xmllint (viene con libxml2)
xmllint --noout --schema DE_v150.xsd mi-documento.xml

# y un test que falle a propósito, para que no vuelva a pasar
assert_equal "Producto Descripción larga",
             texto_sifen("Producto\nDescripción  larga")

Las otras causas de 0160 que nos tocaron

Todas verificadas contra la SET, en orden de cuánto cuesta darse cuenta:

Qué lo dispara Por qué
Código de actividad económica con el prefijo de pantalla, tipo C4_62010 El tipo del XSD es [0-9A-Z]{1,8}: el guion bajo no está permitido. Va el número CIIU pelado, 62010. Ese C4_ es formato de la pantalla de Marangatu, no del documento
Un identificador de envío en cero (dId) Nos pasó por un recorte de cadena que devolvía nil y terminaba enviando 0. El servicio síncrono lo tolera; el de lote responde 0160. Si venís del síncrono, esto aparece justo al migrar
El sobre del lote mal armado El elemento raíz es rEnvioLote —no el que sugiere el nombre del servicio—, el rLoteDE va sin su propio xmlns, y el fichero dentro del ZIP tiene que llamarse xml_file.xml
Cabecera Content-Type equivocada El envío de lote espera application/xml; charset=utf-8. Y en el ambiente de test, la consulta de RUC exige text/xml y rechaza el application/soap+xml estándar — producción sí lo acepta. Misma operación, distinto ambiente
En eventos, los atributos xsi en el nodo equivocado Van en gGroupGesEve, no en rGesEve. Colgados del nodo de más arriba, 0160

La conclusión práctica

El 0160 no se debuguea contra la SET, se debuguea contra el XSD. Los esquemas son públicos y están en la documentación técnica del SIFEN: si los tenés en el proyecto y validás antes de enviar, la mitad de esta lista deja de existir para vos.

Y cuando un rechazo te frene: un rechazo no consume el número ni el CDC. Se corrige y se reenvía el mismo documento. Vale saberlo antes de que entre el pánico.

Si además te toca una fecha de obligatoriedad: cómo se pide la prórroga y quién puede.

Verificado el 26 de julio de 2026 contra el Manual Técnico del SIFEN versión 150 y los XSD de esa misma versión, con documentos reales aprobados por la SET. La documentación técnica se actualiza: comprobá la versión vigente antes de dar algo por hecho.

Esto es información técnica de integración, no asesoramiento tributario. Para el criterio fiscal de tu caso, hablá con tu contador.