Gerar XSD a partir de XML

Infere um esquema inicial. Revise antes de confiar nele.

Entrada
Saída
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 de amostra e isto produz um esqueleto de XSD 1.0 para ele: uma declaração por nome de elemento, xs:sequence para os filhos, xs:complexType onde há filhos ou atributos, xs:simpleContent onde um elemento carrega texto e atributos ao mesmo tempo, e um tipo adivinhado para cada valor. Roda no analisador próprio deste site, num Web Worker, sem nenhum motor para baixar.

Você recorre a isto quando um parceiro mandou cargas de amostra e nenhum esquema, quando precisa de algo para alimentar um gerador de código, ou quando quer uma descrição escrita de um formato que até agora só existiu como «o que o outro sistema manda».

O que torna este diferente é que ele lhe diz o que não consegue saber. Inferir de um documento descreve aquele documento e nada mais: não quais elementos são opcionais, não as faixas de valores reais, não quais ordenações o formato permite. A saída leva esse aviso num comentário, e as seções abaixo dizem quais partes revisar.

O que ele realmente emite

O documento é primeiro varrido, e a inferência é recusada se ele não for bem formado. Depois cada elemento é visitado e uma forma é registrada: quais atributos apareceram e se cada um apareceu todas as vezes, quais filhos apareceram e quantos de cada um sob um mesmo pai, e com que cara estava o texto. Elementos sem filhos e sem atributos são emitidos em linha com um tipo; elementos com filhos recebem um complexType envolvendo uma sequence, na ordem que a amostra usou; elementos com texto e atributos recebem simpleContent com uma extensão.

Três detalhes importam. As formas são indexadas apenas pelo nome do elemento, não pelo caminho, então um <name> sob <customer> e um <name> sob <product> se fundem numa única declaração. Quando um elemento com filhos aparece duas vezes, o segundo é escrito como <xs:element ref="..."/>, que o XSD só resolve contra uma declaração global, então você precisa içá-la. E se um elemento contém texto e filhos, os filhos ganham e o texto é descartado: conteúdo misto exige mixed="true", que nada aqui acrescenta.

Como os tipos são adivinhados, e onde o palpite erra

Os tipos vêm dos caracteres da amostra e de nada mais. Onde o mesmo nome carrega valores de aparência diferente, o palpite se alarga: formas inteiras misturadas dão xs:integer, um inteiro ao lado de um decimal dá xs:decimal, qualquer outra coisa recua para xs:string. Um atributo que parece de outro tipo numa segunda ocorrência cai para xs:string.

O modo de falha decorre disso. Um campo de status contendo «1» e «2» é tipado como inteiro, e então o esquema rejeita o «N/D» que esse campo carrega uma vez por mês. Um código de produto «0123» vira número, o que perde o zero à esquerda e faz «0123» e «123» serem o mesmo valor. Uma salvaguarda corre no sentido oposto: dígitos longos demais para serem um inteiro seguro, um número de conta de 20 dígitos por exemplo, ficam como cadeia em vez de virarem um número que perde precisão.

  • Um elemento vazio dá xs:string, porque do nada não se infere nada.
  • Só dígitos dá xs:nonNegativeInteger, ou xs:integer com um sinal de menos à frente. Dígitos com ponto decimal dão xs:decimal; notação científica recai em xs:string.
  • Exatamente «true» ou «false» dá xs:boolean. «1», «yes» e «Y» não.
  • AAAA-MM-DD dá xs:date, e o mesmo seguido de T e de uma hora dá xs:dateTime. Qualquer outro formato de data é cadeia.
  • Um valor começando por http:// ou https:// dá xs:anyURI. Outros esquemas e caminhos relativos não.

O que uma amostra só não consegue lhe dizer

A ocorrência é a maior lacuna. Um elemento é marcado com minOccurs="0" só onde a amostra o mostrou ausente: ele apareceu pela primeira vez sob um pai posterior, ou um pai que o tinha uma vez foi visto de novo sem ele. Um campo que é opcional no formato mas está presente em toda a sua amostra sai como obrigatório, e amanhã o esquema vai rejeitar um documento legítimo. O maxOccurs também é grosseiro: qualquer coisa vista mais de uma vez sob um mesmo pai se torna unbounded, então um par que é sempre exatamente dois fica ilimitado.

A ordem é afirmada, não inferida. xs:sequence diz que os filhos precisam aparecer naquela ordem, que é o que a amostra mostrou e pode não ser o que o formato exige. Se a ordem não importa, use xs:all, que o XSD 1.0 só permite no topo de um modelo de conteúdo e com cada elemento no máximo uma vez; se os filhos são alternativas, use xs:choice. Nada infere faixas, enumerações, padrões, xs:key nem xs:keyref, e é ali que moram as regras de negócio.

Espaços de nomes são o limite mais afiado. Nomes de elemento são tomados exatamente como escritos, prefixo incluído, então um documento contendo <dc:title> produz <xs:element name="dc:title">, e o nome de uma declaração de elemento precisa ser um NCName, que não pode conter dois-pontos. Esse esquema não vai compilar.

A lista de revisão

Antes que um esquema gerado chegue perto de um pipeline de build ou de um parceiro, percorra esta lista. Comece passando a saída de volta pelo validador de XSD contra a mesma amostra: um esquema que não consegue validar o documento de onde veio esbarrou num dos casos acima.

  • Cada minOccurs. Quais campos são de fato obrigatórios e quais apenas estavam presentes na sua amostra?
  • Cada maxOccurs="unbounded". Existe um limite superior real?
  • Cada tipo, com mais rigor em códigos, identificadores e status que saíram como inteiros ou booleanos.
  • xs:sequence, e se deveria ser xs:all ou xs:choice.
  • targetNamespace e os nomes de elemento, se a amostra usava espaços de nomes.
  • mixed="true" em qualquer elemento que carregue texto ao lado dos filhos.
  • Todo xs:element ref, que precisa de uma declaração global para apontar.
  • Nomes fundidos: um nome de elemento que significa duas coisas em dois lugares precisa de dois tipos.
  • O que nada consegue inferir: enumerações, padrões, faixas, chaves e referências de chave.

Inferir um esquema em código

Inferência de esquema não está em nenhuma biblioteca padrão exceto a do .NET, então estas ferramentas divergem mais do que o habitual. Dê o mesmo documento a todas e você recebe esquemas diferentes: as diferenças estão todas nos palpites.

// No dependency: DOMParser is enough to collect the shape of a document.
// This prints the inventory a schema is built from, which is the part worth
// reading before you trust any generator's output.
function inventory(xmlText) {
  const doc = new DOMParser().parseFromString(xmlText, 'application/xml');
  if (doc.querySelector('parsererror')) throw new Error('not well-formed');

  const shapes = new Map();

  const visit = (el) => {
    let shape = shapes.get(el.tagName);
    if (!shape) {
      shape = { count: 0, attrs: new Map(), children: new Map(), values: new Set() };
      shapes.set(el.tagName, shape);
    }
    shape.count++;

    for (const a of el.attributes) {
      if (a.name === 'xmlns' || a.name.startsWith('xmlns:')) continue;
      shape.attrs.set(a.name, (shape.attrs.get(a.name) ?? 0) + 1);
    }

    const kids = [...el.children];
    const seen = new Map();
    for (const c of kids) seen.set(c.tagName, (seen.get(c.tagName) ?? 0) + 1);
    for (const [name, n] of seen) {
      const m = shape.children.get(name) ?? { min: Infinity, max: 0 };
      shape.children.set(name, { min: Math.min(m.min, n), max: Math.max(m.max, n) });
    }
    if (!kids.length && el.textContent.trim()) shape.values.add(el.textContent.trim());

    kids.forEach(visit);
  };
  visit(doc.documentElement);

  for (const [name, s] of shapes) {
    // An attribute seen fewer times than its element is optional. Present on
    // every occurrence proves nothing: it may still be optional in the format.
    const optional = [...s.attrs].filter(([, n]) => n < s.count).map(([a]) => a);
    console.log(name, 'x' + s.count, 'optional attrs:', optional.join(', ') || 'none');
  }
}
# pip install defusedxml
# The standard library parser is not safe on input you did not write.
from collections import defaultdict
from defusedxml.ElementTree import parse

def inventory(path):
    root = parse(path).getroot()
    shapes = defaultdict(lambda: {'count': 0, 'attrs': defaultdict(int),
                                  'children': {}, 'values': set()})

    def visit(el):
        s = shapes[el.tag]
        s['count'] += 1
        for name in el.attrib:
            s['attrs'][name] += 1

        counts = defaultdict(int)
        for c in el:
            counts[c.tag] += 1
        for name, n in counts.items():
            lo, hi = s['children'].get(name, (n, n))
            s['children'][name] = (min(lo, n), max(hi, n))
        if len(el) == 0 and (el.text or '').strip():
            s['values'].add(el.text.strip())

        for c in el:
            visit(c)

    visit(root)

    for tag, s in shapes.items():
        optional = [a for a, n in s['attrs'].items() if n < s['count']]
        print(f"{tag} x{s['count']}  optional attributes: {optional or 'none'}")
        for name, (lo, hi) in s['children'].items():
            # lo == 0 is never inferable from a single occurrence of the parent.
            print(f"  {name}: seen {lo}..{hi} per parent")

inventory('sample.xml')
// Apache XMLBeans: org.apache.xmlbeans:xmlbeans:5.2.1
// Inst2Xsd is the closest thing Java has to a standard inference tool, and it
// takes several instance documents, which is the main thing this page cannot.
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.impl.inst2xsd.Inst2Xsd;
import org.apache.xmlbeans.impl.inst2xsd.Inst2XsdOptions;
import org.apache.xmlbeans.impl.xb.xsdschema.SchemaDocument;
import java.io.File;

XmlObject[] instances = new XmlObject[] {
    XmlObject.Factory.parse(new File("sample-1.xml")),
    XmlObject.Factory.parse(new File("sample-2.xml")),   // more samples, better guesses
};

Inst2XsdOptions options = new Inst2XsdOptions();
// RUSSIAN_DOLL nests everything; SALAMI_SLICE makes every element global,
// which is easier to hand-edit afterwards.
options.setDesign(Inst2XsdOptions.DESIGN_SALAMI_SLICE);
options.setSimpleContentTypes(Inst2XsdOptions.SIMPLE_CONTENT_TYPES_SMART);
options.setUseEnumerations(Inst2XsdOptions.ENUMERATION_NEVER);

SchemaDocument[] schemas = Inst2Xsd.inst2xsd(instances, options);
for (int i = 0; i < schemas.length; i++) {
    schemas[i].save(new File("inferred-" + i + ".xsd"));
}
using System.Xml;
using System.Xml.Schema;

// XmlSchemaInference is in the framework: no package needed. It is also the
// only one of these that will refine an existing schema with a new sample.
var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null,
};

var inference = new XmlSchemaInference
{
    // Relaxed: string everywhere. Restricted: guess int, date, boolean and so
    // on, with all the risk that implies for codes and identifiers.
    TypeInference = XmlSchemaInference.InferenceOption.Restricted,
    Occurrence = XmlSchemaInference.InferenceOption.Relaxed,
};

XmlSchemaSet schemas;
using (var reader = XmlReader.Create("sample-1.xml", settings))
{
    schemas = inference.InferSchema(reader);
}
using (var reader = XmlReader.Create("sample-2.xml", settings))
{
    schemas = inference.InferSchema(reader, schemas);   // widen with a second sample
}

using var output = new XmlTextWriter("inferred.xsd", null) { Formatting = Formatting.Indented };
foreach (XmlSchema schema in schemas.Schemas())
{
    schema.Write(output);
}
# Trang, from the RELAX NG authors, is the best command-line option and takes
# as many samples as you can give it.
java -jar trang.jar -I xml -O xsd sample-1.xml sample-2.xml sample-3.xml inferred.xsd

# It will emit a DTD or a RELAX NG schema from the same input:
java -jar trang.jar -I xml -O dtd sample-1.xml inferred.dtd

# Then do the step most people skip: check the schema against the documents it
# was inferred from before trusting it.
xmllint --noout --nonet --schema inferred.xsd sample-1.xml

O recurso que vale perseguir em outro lugar são as múltiplas amostras. Trang, XMLBeans e XmlSchemaInference aceitam todos vários documentos e alargam o resultado, o que transforma «este campo estava sempre presente» em «este campo às vezes falta» sem você ter de adivinhar.

Perguntas frequentes

Meu documento de amostra é enviado para gerar o esquema?

Não. A inferência roda no analisador próprio deste site, em JavaScript, num Web Worker desta aba. Não há servidor envolvido e, ao contrário do validador de esquema, nem mesmo um motor WebAssembly para baixar.

Isso vale mais aqui do que parece: uma amostra é, por definição, uma carga real, com nomes de clientes, números de conta e preços verdadeiros dentro.

Posso gerar um esquema a partir de mais de um documento de amostra?

Aqui não. Esta página infere do único documento que está no editor, que é o limite honesto de uma ferramenta feita para responder num só colar.

Mais amostras produzem de fato um esquema melhor, porque são o único jeito de aprender que um campo é opcional. O Trang aceita quantos arquivos de entrada você quiser, o Inst2Xsd do XMLBeans aceita um vetor de instâncias, e o XmlSchemaInference refina um esquema existente com mais uma amostra. Fora isso, gere a partir da sua maior amostra e relaxe os valores de minOccurs à mão.

Por que meu CEP saiu como inteiro?

Porque na amostra eram dígitos, e nada num único documento distingue um número de um código que por acaso é numérico.

É a coisa mais comum a corrigir numa saída gerada. CEPs, códigos de produto, números de telefone, referências de conta e qualquer coisa com zero à esquerda deveriam quase sempre ser xs:string, às vezes com um xs:pattern.

Meu documento usa espaços de nomes e o esquema não compila. E agora?

Isso é esperado. Nomes de elemento são tomados exatamente como escritos, então <dc:title> vira <xs:element name="dc:title">, e o nome de uma declaração de elemento precisa ser um NCName, que não pode conter dois-pontos.

Trate a saída como um inventário estrutural. Acrescente um targetNamespace ao elemento xs:schema e retire os prefixos dos atributos name. O elementFormDefault é escrito como «qualified»: os filhos ficam no mesmo espaço de nomes do pai.

A saída é XSD 1.0 ou 1.1?

XSD 1.0, e nada nela usa uma construção que o 1.1 acrescentou. Isso é deliberado: 1.0 é o que libxml2, .NET, lxml e xmllint todos implementam, enquanto um esquema 1.1 funciona no Xerces-J, no Saxon-EE e no pacote xmlschema do Python, e em nenhum outro lugar.

Uma regra entre campos como «o fim não pode ser antes do início» precisa de xs:assert, que só existe no 1.1. Acrescente essas à mão, na versão que os seus consumidores conseguem processar.

Como eu verifico se o esquema gerado presta?

Valide a amostra contra ele, no validador de XSD deste site. Um esquema que não consegue validar a própria fonte esbarrou num dos casos conhecidos: um nome de elemento com espaço de nomes, conteúdo misto, ou um ref apontando para uma declaração que não é global.

Depois tente contra um documento que ele nunca viu, de preferência de outro dia ou de outro cliente. É ali que os valores errados de minOccurs vêm à superfície.

Ferramentas relacionadas

Leitura complementar