Generar un XSD a partir de XML
Infiere un esquema de partida. Revísalo antes de confiar en él.
Todo se ejecuta en esta pestaña. Nada de lo que pegues se sube, se registra ni se envía a ningún sitio. Abre el panel de red y compruébalo.
Pega un documento de muestra y esto produce un esqueleto de XSD 1.0 para él: una declaración por nombre de elemento, xs:sequence para los hijos, xs:complexType donde hay hijos o atributos, xs:simpleContent donde un elemento lleva a la vez texto y atributos, y un tipo adivinado para cada valor. Funciona con el escáner propio de este sitio en un Web Worker, sin ningún motor que descargar.
Recurres a esto cuando un socio ha enviado cargas de muestra y ningún esquema, cuando necesitas algo con lo que alimentar un generador de código, o cuando quieres una descripción escrita de un formato que hasta ahora solo ha existido como «lo que sea que manda el otro sistema».
Lo que hace distinto a este es que te dice lo que no puede saber. Inferir a partir de un documento describe ese documento y nada más: no qué elementos son opcionales, no los rangos de valores reales, no qué ordenaciones permite el formato. La salida lleva ese aviso en un comentario, y las secciones de abajo dicen qué partes hay que revisar.
Qué emite en realidad
El documento se escanea primero, y la inferencia se rechaza si no está bien formado. Luego se visita cada elemento y se registra una forma: qué atributos aparecieron y si cada uno apareció siempre, qué hijos aparecieron y cuántos de cada uno bajo un mismo padre, y qué aspecto tenía el texto. Los elementos sin hijos y sin atributos se emiten en línea con un tipo; los elementos con hijos reciben un complexType que envuelve una sequence, en el orden que usó la muestra; los elementos con texto y atributos reciben simpleContent con una extensión.
Importan tres detalles. Las formas se indexan solo por el nombre del elemento, no por la ruta, así que un <name> bajo <customer> y un <name> bajo <product> se fusionan en una sola declaración. Cuando un elemento con hijos aparece dos veces, el segundo se escribe como <xs:element ref="..."/>, que XSD resuelve solo contra una declaración global, así que tienes que elevarla. Y si un elemento contiene a la vez texto e hijos, ganan los hijos y el texto se descarta: el contenido mixto necesita mixed="true", que aquí nada añade.
Cómo se adivinan los tipos, y dónde la adivinanza falla
Los tipos salen de los caracteres de la muestra y de nada más. Donde el mismo nombre lleva valores de aspecto distinto, la adivinanza se ensancha: formas entero mezcladas dan xs:integer, un entero junto a un decimal da xs:decimal, cualquier otra cosa cae a xs:string. Un atributo que parece de otro tipo en una segunda aparición baja a xs:string.
El modo de fallo se sigue de ahí. Un campo de estado que lleva «1» y «2» se tipa como entero, y entonces el esquema rechaza el «N/D» que ese campo lleva una vez al mes. Un código de producto «0123» se convierte en número, lo que pierde el cero inicial y hace que «0123» y «123» sean el mismo valor. Una salvaguarda va en la otra dirección: dígitos demasiado largos para ser un entero seguro, un número de cuenta de 20 cifras por ejemplo, se quedan como cadena en vez de convertirse en un número que pierde precisión.
- Un elemento vacío da xs:string, porque de la nada no se puede inferir nada.
- Solo dígitos da xs:nonNegativeInteger, o xs:integer con un signo menos delante. Dígitos con punto decimal dan xs:decimal; la notación científica cae a xs:string.
- Exactamente «true» o «false» da xs:boolean. «1», «yes» y «Y» no.
- AAAA-MM-DD da xs:date, y lo mismo seguido de T y una hora da xs:dateTime. Cualquier otro formato de fecha es una cadena.
- Un valor que empieza por http:// o https:// da xs:anyURI. Otros esquemas y las rutas relativas no.
Lo que una sola muestra no puede decirte
La aparición es la mayor laguna. Un elemento se marca con minOccurs="0" solo donde la muestra lo mostró ausente: apareció por primera vez bajo un padre posterior, o un padre que lo tuvo una vez se vio de nuevo sin él. Un campo que es opcional en el formato pero está presente en toda tu muestra sale como obligatorio, y mañana el esquema rechazará un documento legal. maxOccurs también es grueso: cualquier cosa vista más de una vez bajo un mismo padre se vuelve unbounded, así que un par que siempre son exactamente dos queda ilimitado.
El orden se afirma, no se infiere. xs:sequence dice que los hijos deben aparecer en ese orden, que es lo que mostró la muestra y puede no ser lo que el formato exige. Si el orden no importa, usa xs:all, que XSD 1.0 solo permite en la cima de un modelo de contenido y con cada elemento como máximo una vez; si los hijos son alternativas, usa xs:choice. Nada infiere rangos, enumeraciones, patrones, xs:key ni xs:keyref, y ahí están las reglas de negocio.
Los espacios de nombres son el límite más afilado. Los nombres de elemento se toman exactamente como están escritos, prefijo incluido, así que un documento que contiene <dc:title> produce <xs:element name="dc:title">, y el nombre de una declaración de elemento debe ser un NCName, que no puede contener dos puntos. Ese esquema no compilará.
La lista de repaso
Antes de que un esquema generado se acerque a una canalización de compilación o a un socio, recorre esta lista. Empieza pasando la salida de vuelta por el validador de XSD contra la misma muestra: un esquema que no puede validar el documento del que salió ha topado con uno de los casos de arriba.
- Cada minOccurs. ¿Qué campos son de verdad obligatorios y cuáles solo estaban presentes en tu muestra?
- Cada maxOccurs="unbounded". ¿Hay un límite superior real?
- Cada tipo, con más dureza en códigos, identificadores y estados que salieron como enteros o booleanos.
- xs:sequence, y si debería ser xs:all o xs:choice.
- targetNamespace y los nombres de elemento, si la muestra usaba espacios de nombres.
- mixed="true" en cualquier elemento que lleve texto junto a sus hijos.
- Cualquier xs:element ref, que necesita una declaración global a la que apuntar.
- Nombres fusionados: un nombre de elemento que significa dos cosas en dos sitios necesita dos tipos.
- Lo que nada puede inferir: enumeraciones, patrones, rangos, claves y referencias de clave.
Inferir un esquema desde código
La inferencia de esquemas no está en ninguna biblioteca estándar salvo la de .NET, así que estas herramientas difieren más de lo habitual. Da el mismo documento a todas y obtienes esquemas distintos: las diferencias están todas en las adivinanzas.
// 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.xmlLa capacidad que merece la pena buscar en otra parte son las muestras múltiples. Trang, XMLBeans y XmlSchemaInference aceptan todos varios documentos y ensanchan el resultado, lo que convierte «este campo estaba siempre presente» en «este campo a veces falta» sin que tengas que adivinarlo.
Preguntas frecuentes
¿Se sube mi documento de muestra para generar el esquema?
No. La inferencia corre con el escáner propio de este sitio, en JavaScript, en un Web Worker de esta pestaña. No hay servidor implicado y, a diferencia del validador de esquemas, ni siquiera un motor WebAssembly que descargar.
Eso vale más aquí de lo que parece: una muestra es por definición una carga real, con nombres de clientes, números de cuenta y precios de verdad dentro.
¿Puedo generar un esquema a partir de más de un documento de muestra?
Aquí no. Esta página infiere del único documento que hay en el editor, que es el límite honesto de una herramienta hecha para responder en un solo pegado.
Más muestras sí producen un esquema mejor, porque son la única forma de aprender que un campo es opcional. Trang acepta cualquier número de archivos de entrada, Inst2Xsd de XMLBeans acepta un array de instancias, y XmlSchemaInference refina un esquema existente con una muestra más. Si no, genera desde tu muestra más grande y relaja los valores de minOccurs a mano.
¿Por qué mi código postal salió como entero?
Porque en la muestra eran dígitos, y nada en un solo documento distingue un número de un código que resulta ser numérico.
Esto es lo más común que hay que arreglar en la salida generada. Códigos postales, códigos de producto, números de teléfono, referencias de cuenta y cualquier cosa con un cero inicial deberían ser casi siempre xs:string, a veces con un xs:pattern.
Mi documento usa espacios de nombres y el esquema no compila. ¿Y ahora?
Es lo esperado. Los nombres de elemento se toman exactamente como están escritos, así que <dc:title> se convierte en <xs:element name="dc:title">, y el nombre de una declaración de elemento debe ser un NCName, que no puede contener dos puntos.
Trata la salida como un inventario estructural. Añade un targetNamespace al elemento xs:schema y quita los prefijos de los atributos name. elementFormDefault se escribe como «qualified»: los hijos están en el mismo espacio de nombres que su padre.
¿La salida es XSD 1.0 o 1.1?
XSD 1.0, y nada en ella usa una construcción que añadió 1.1. Es deliberado: 1.0 es lo que implementan libxml2, .NET, lxml y xmllint, mientras que un esquema 1.1 funciona en Xerces-J, Saxon-EE y el paquete xmlschema de Python, y en ningún otro sitio.
Una regla entre campos como «fin no debe ser anterior a inicio» necesita xs:assert, que es solo de 1.1. Añade esas a mano, en la versión que tus consumidores puedan procesar.
¿Cómo compruebo que el esquema generado sirve de algo?
Valida la muestra contra él, en el validador de XSD de este sitio. Un esquema que no puede validar su propia fuente ha topado con uno de los casos conocidos: un nombre de elemento con espacio de nombres, contenido mixto, o un ref que apunta a una declaración que no es global.
Luego pruébalo contra un documento que nunca haya visto, idealmente de otro día o de otro cliente. Ahí es donde afloran los valores de minOccurs equivocados.