Verificador de sintaxe XML

Todos os erros de sintaxe de uma vez, com linha, coluna e correção.

Entrada
AguardandoCole um documento para verificá-lo. A validação roda enquanto você digita.

Tudo roda nesta aba. Nada do que você colar é enviado, registrado ou transmitido para lugar nenhum. Abra o painel de rede e confira.

Cole um documento acima, ou solte um arquivo sobre o editor, e ele é verificado contra as regras de boa formação do XML 1.0 enquanto você digita. Cada violação é listada com linha, coluna e o que escrever no lugar, e clicar num resultado leva o cursor até o caractere problemático.

Esta é a verificação que precisa passar antes que qualquer outra possa rodar. Um validador de esquema, um processador XSLT, uma pilha SOAP e um navegador se recusam a olhar um documento com a sintaxe quebrada, então um "premature end of data in tag" saindo do seu build é problema de sintaxe, e ler o XSD não vai explicar isso.

O que é diferente aqui é que o scanner não para na primeira falha. Ele se recupera e segue, então uma única passagem reporta o atributo sem aspas na linha 4, o & solto na linha 12 e a tag deixada aberta no fim. Cada uma das 37 classes de erro tem a própria redação e a própria correção, em vez de um "Syntax Error" para tudo. Nada é enviado: abra o painel de rede e veja que ele continua vazio.

O que "bem formado" realmente significa

O XML 1.0 nomeia doze restrições de boa formação e deixa o resto na gramática. Reduzido a verificações que um documento passa ou não passa, dá esta lista, e cada linha dela é aplicada aqui.

  • Exatamente um elemento raiz, sem nada fora dele além de comentários, instruções de processamento e espaço em branco.
  • Toda tag de abertura fechada por uma de fechamento cujo nome combine exatamente. Nomes diferenciam maiúsculas, então </Note> não fecha <note>.
  • Elementos aninhados, não sobrepostos: <b><i>x</b></i> não é XML, por mais tolerante que o seu analisador de HTML tenha sido.
  • Valores de atributo entre aspas, e nenhum atributo repetido no mesmo elemento.
  • Nenhum < literal no texto nem em valor de atributo, e nenhum & solto fora de uma referência. Um > é legal no texto; a sequência ]]> não é.
  • Apenas as cinco entidades predefinidas — amp, lt, gt, quot e apos — a não ser que uma DTD declare outras. &nbsp; é de HTML e não é uma delas.
  • Nenhum caractere fora do conjunto que o XML permite, nenhum comentário contendo --, e uma declaração, se houver, no começo do arquivo e escrita como version, depois encoding, depois standalone.
  • Todo prefixo de namespace vinculado por uma declaração xmlns em escopo. Essa regra vem de Namespaces in XML, não do XML 1.0, e é a que mais validadores gratuitos pulam: um concorrente de peso dá <ns:root> como válido sem nenhuma vinculação no arquivo.

Todos os erros de uma vez, e por que outras ferramentas dão um

A especificação diz ao processador que ele não deve continuar o processamento normal depois de um erro fatal, e é por isso que DOMParser, Expat e .NET reportam o primeiro problema e param. Isso é comportamento correto, não preguiça, e também é o motivo de consertar um documento grande custar uma ida e volta por erro.

A recuperação aqui é deliberada. Quando uma tag de fechamento não bate, o scanner procura aquele nome mais acima na pilha de elementos abertos: encontrá-lo significa que o erro real são os elementos deixados abertos no meio, então cada um é reportado na própria linha de abertura em vez de culpar a tag de fechamento. Um caractere estranho dentro de um nome absorve o token inteiro, então amount$="10" gera uma mensagem sobre o cifrão em vez de cinco sobre o sinal de igual e as aspas. Uma ressalva: um erro estrutural logo no começo faz tudo depois dele parecer errado, então conserte os dois ou três primeiros e verifique de novo. O relatório para em 200 problemas, com uma nota dizendo isso.

As falhas que realmente aparecem

Erros de sintaxe reais raramente são exóticos. Eles custam tempo porque o analisador nomeia um sintoma enquanto a causa está em outro lugar, ou é invisível.

  • Uma marca de ordem de bytes UTF-8 antes da declaração. Legal, invisível e a causa habitual de "Content is not allowed in prolog". Aqui é reportada como ela mesma, e não como conteúdo misterioso.
  • Uma declaração escrita como <?xml encoding="UTF-8" version="1.0"?>, ou uma que diz windows-1252 enquanto os bytes são UTF-8. O segundo caso vira um aviso sobre o que um analisador em nível de bytes vai ler, porque o arquivo já chegou decodificado a esta página.
  • &nbsp;, &mdash; e cerca de cinquenta outras entidades HTML, nomeadas como HTML e não como entidade indefinida genérica, com a referência numérica a usar no lugar.
  • Um aviso do PHP ou um stack trace grudado depois da tag raiz de fechamento, e é por isso que essa mensagem aponta para o corpo HTTP cru.
  • Caracteres de controle vindos de uma coluna de banco, que o XML 1.0 proíbe em todo lugar, CDATA incluído.
  • Um prefixo soap:, xsi: ou xlink: sem vínculo, depois de um trecho ter sido copiado de um documento maior deixando a declaração xmlns na raiz original.

Onde a verificação de sintaxe para

Esta página responde a uma pergunta: a sintaxe é legal. Ela nunca pergunta se um <invoice> pode conter uma <line>, nem se falta um elemento obrigatório. Isso precisa de um esquema, então mora nos validadores de XSD e de DTD. Se um sistema chamou o seu documento de "inválido", ele rodou uma verificação de esquema, e um resultado verde aqui não vai explicar isso.

Dois limites que valem ser ditos. O subconjunto interno da DTD é lido apenas para declarações de entidade, então declarações de elemento e de lista de atributos ali dentro são atravessadas sem serem aplicadas, e DTDs externas são reportadas mas nunca buscadas. O tamanho tem teto de 20 milhões de caracteres, o aninhamento de 512 níveis e a expansão de entidades de 10 milhões, porque tudo roda nesta aba.

Verificando a sintaxe XML em código

A mesma verificação nas linguagens que mais consomem XML, cada uma na forma segura contra entidades externas e expansão de entidades. Repare em quão poucas reportam mais de um erro: isso é a especificação falando, não a biblioteca.

// DOMParser never throws. Given broken input it returns a document whose
// root is <parsererror>, which is why so much code silently accepts XML
// that no other parser would take.
function checkXml(source) {
  const doc = new DOMParser().parseFromString(source, 'application/xml');
  const error = doc.querySelector('parsererror');
  if (!error) return { wellFormed: true, message: null };

  // The text is browser-specific. Firefox includes a line and column,
  // Chrome's wording differs, and neither exposes them as fields.
  return { wellFormed: false, message: error.textContent.trim() };
}

// Browsers do not resolve external entities, so XXE is not reachable here.
// Internal entity expansion is, so cap the input length before parsing:
//   if (source.length > 20_000_000) throw new Error('too large');
# lxml is the only common Python parser that keeps going after a syntax
# error and hands back the whole list rather than the first entry.
from lxml import etree

parser = etree.XMLParser(
    recover=True,            # keep parsing so error_log fills up
    resolve_entities=False,  # do not expand entities
    no_network=True,         # never fetch an external DTD
    load_dtd=False,
    huge_tree=False,         # keep libxml2's depth and expansion limits
)
etree.fromstring(source.encode('utf-8'), parser)

for entry in parser.error_log:
    print(f"line {entry.line}, column {entry.column}: {entry.message}")
if not parser.error_log:
    print('well-formed')

# Without recover=True, fromstring raises etree.XMLSyntaxError and
# e.position gives a (line, column) tuple for the first failure only.
# For untrusted input with the standard library, use defusedxml.
import javax.xml.XMLConstants;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// Drop the next line only if your documents legitimately carry a DOCTYPE.
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

var reader = factory.newSAXParser().getXMLReader();
reader.setErrorHandler(new DefaultHandler() {
    @Override public void warning(SAXParseException e) { print("warning", e); }
    @Override public void error(SAXParseException e) { print("error", e); }
    @Override public void fatalError(SAXParseException e) throws SAXParseException {
        print("fatal", e);
        throw e;  // the specification requires processing to stop here
    }
    private void print(String kind, SAXParseException e) {
        System.err.printf("%s at line %d, column %d: %s%n",
            kind, e.getLineNumber(), e.getColumnNumber(), e.getMessage());
    }
});
reader.parse(new InputSource(new java.io.StringReader(source)));
using System.Xml;

var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,  // no DTD, so no entity expansion
    XmlResolver = null,                      // never fetch anything
    MaxCharactersFromEntities = 1024 * 1024,
    MaxCharactersInDocument = 20L * 1024 * 1024,
    ConformanceLevel = ConformanceLevel.Document,
};

try
{
    using var reader = XmlReader.Create(new StringReader(source), settings);
    while (reader.Read()) { }   // pull every node; syntax errors surface here
    Console.WriteLine("well-formed");
}
catch (XmlException e)
{
    // LinePosition is the column, 1-based. Reading stops at this point.
    Console.Error.WriteLine(
        $"line {e.LineNumber}, column {e.LinePosition}: {e.Message}");
}
<?php
// libxml recovers internally, so libxml_get_errors() is the closest a PHP
// script gets to a full list rather than a first failure.
libxml_use_internal_errors(true);

$doc = new DOMDocument();
$doc->loadXML($source, LIBXML_NONET);   // NONET blocks external fetches

$errors = libxml_get_errors();
libxml_clear_errors();

foreach ($errors as $e) {
    $kind = $e->level === LIBXML_ERR_FATAL ? 'fatal' : 'error';
    fprintf(
        STDERR,
        "%s at line %d, column %d: %s\n",
        $kind,
        $e->line,
        $e->column,
        trim($e->message)
    );
}
exit($errors ? 1 : 0);
# xmllint ships with libxml2 and is already installed on most machines.
# --nonet stops it fetching a DTD the document points at.
xmllint --noout --nonet document.xml

# --recover keeps parsing after a failure, so you see more than the first.
xmllint --noout --nonet --recover document.xml

# Check a whole tree, one file at a time:
find . -name '*.xml' -print0 | xargs -0 -n1 xmllint --noout --nonet

# Exit status: 0 well-formed, 1 unclassified, 3 well-formedness error,
# 4 validation error against a DTD or schema.

Todo exemplo desliga alguma coisa, e isso é o conteúdo, não formalidade. A resolução de entidades externas vem ligada por padrão em Java, e a biblioteca padrão do Python não aplica limite de expansão nenhum, então essas flags são o que separa uma verificação de sintaxe de uma primitiva de leitura de arquivos apontada para o seu próprio servidor.

Perguntas frequentes

Como verifico a sintaxe XML sem instalar nada?

Cole o documento no editor no topo desta página. A verificação roda enquanto você digita, sem botão para apertar e sem conta para criar. Você também pode soltar um arquivo .xml em cima: ele é lido pela File API do navegador e nunca é transmitido.

No terminal, o xmllint faz parte do libxml2 e já vem no macOS e na maioria das distribuições Linux: xmllint --noout --nonet seuarquivo.xml não imprime nada quando a sintaxe está legal. Ele para no primeiro erro, que é a diferença prática.

Meu XML é enviado quando eu verifico aqui?

Não. O scanner é JavaScript rodando nesta aba, num Web Worker. Não existe endpoint para onde mandar um documento, nem analytics ou script de relatório de erros com acesso ao editor.

Isso é verificável, não é promessa. Abra as ferramentas de desenvolvedor, vá para a aba Rede, cole um documento e observe: os recursos desta página carregam uma vez e nada mais acontece. Importa porque os documentos que as pessoas colam em verificadores de sintaxe são envelopes SOAP com tokens, asserções SAML e arquivos de configuração com strings de conexão.

Qual a diferença entre verificador de sintaxe e validador XML?

Os dois são usados como sinônimos, e é daí que vem a confusão. Uma verificação de sintaxe pergunta se o documento obedece às regras do próprio XML: tags fechadas, aninhamento correto, uma raiz, atributos entre aspas, "e comercial" escapado. Ela não precisa de nada além do documento.

Validação no sentido da especificação pergunta se o documento corresponde a um esquema, um XSD ou uma DTD: um segundo arquivo descrevendo quais elementos podem aparecer onde. Ela não roda enquanto a sintaxe não estiver limpa, e por isso um "inválido" vindo de uma pilha de middleware às vezes quer dizer erro de sintaxe e às vezes violação de esquema. Esta página faz a primeira verificação e diz isso; os validadores de XSD e DTD daqui fazem a segunda, também sem enviar nada.

Por que isto reporta cinco erros quando meu editor reporta um?

Porque a especificação XML manda um processador conforme parar depois de um erro fatal, e o analisador por trás do seu editor faz isso. A regra se defende: uma vez que uma tag ficou sem fechar, o analisador realmente não sabe o que o resto do documento queria dizer.

Este scanner se recupera: ele se ressincroniza nas fronteiras das tags, absorve inteiro um token de nome defeituoso e prefere culpar uma tag de abertura não fechada a culpar a de fechamento que a revelou. Trate os erros mais no começo como os confiáveis e verifique de novo, já que uma lista longa costuma ter um único erro estrutural perto do topo gerando quase tudo.

Nomes de tag XML diferenciam maiúsculas de minúsculas?

Completamente. <Note> e <note> são elementos diferentes, e </Note> não fecha <note>. Isso pega quem vem do HTML, onde o analisador converte nomes de tag para minúsculas, e é a causa mais comum de erro de tags que não batem em XML escrito à mão.

Vale também para nomes de atributo e prefixos de namespace: xmlns:Soap e xmlns:soap são prefixos diferentes. Quando a divergência é só de caixa, a mensagem diz isso em vez de reportar um descompasso genérico. Vários resumos muito copiados de "regras de sintaxe XML" afirmam que tags XML não diferenciam maiúsculas; estão errados, e segui-los produz documentos que todo analisador real rejeita.

Qual o maior arquivo que ele consegue verificar?

20 milhões de caracteres, mais ou menos 20 MB de ASCII. Um documento de 10 MB é varrido em pouco mais de um segundo e 1 MB em cerca de 110 milissegundos, num Web Worker, então o editor continua respondendo.

Para algo maior, use um analisador em streaming localmente: xmllint, o iterparse do Python ou qualquer analisador SAX verifica a boa formação sem montar a árvore inteira.

Ferramentas relacionadas

Leitura complementar

Erros que isto resolve