Generare un XSD da XML
Deduce uno schema di partenza. Rileggilo prima di fidarti.
Tutto viene eseguito in questa scheda. Nulla di ciò che incolli viene caricato, registrato o inviato da qualche parte. Apri il pannello di rete e verifica.
Incolla un documento di esempio e questo ne produce uno scheletro XSD 1.0: una dichiarazione per nome di elemento, xs:sequence per i figli, xs:complexType dove ci sono figli o attributi, xs:simpleContent dove un elemento porta sia testo sia attributi, e un tipo indovinato per ogni valore. Gira sullo scanner di questo sito, in un Web Worker, senza alcun motore da scaricare.
Ci si ricorre quando un partner ha mandato payload di esempio e nessuno schema, quando ti serve qualcosa da dare in pasto a un generatore di codice, o quando vuoi una descrizione scritta di un formato che finora è esistito solo come «quello che manda l’altro sistema».
Ciò che rende diverso questo strumento è che ti dice quel che non può sapere. Dedurre da un solo documento descrive quel documento e nient’altro: non quali elementi sono facoltativi, non gli intervalli di valori reali, non quali ordinamenti il formato consente. L’output porta quell’avvertimento in un commento, e le sezioni qui sotto dicono quali parti rivedere.
Che cosa produce davvero
Il documento viene prima scansionato, e la deduzione è rifiutata se non è ben formato. Poi ogni elemento viene visitato e ne viene registrata una forma: quali attributi sono comparsi e se ciascuno è comparso ogni volta, quali figli sono comparsi e quanti di ciascuno sotto uno stesso genitore, e come si presentava il testo. Gli elementi senza figli e senza attributi vengono emessi in linea con un tipo; gli elementi con figli ottengono un complexType che avvolge una sequence, nell’ordine usato dall’esempio; gli elementi con testo e attributi ottengono simpleContent con un’estensione.
Contano tre dettagli. Le forme sono indicizzate sul solo nome dell’elemento, non sul percorso, quindi un <name> sotto <customer> e un <name> sotto <product> si fondono in un’unica dichiarazione. Quando un elemento con figli compare due volte, il secondo viene scritto come <xs:element ref="..."/>, che XSD risolve soltanto contro una dichiarazione globale: devi quindi portarla fuori. E se un elemento contiene sia testo sia figli, vincono i figli e il testo viene perso: il contenuto misto richiede mixed="true", che qui nulla aggiunge.
Come vengono indovinati i tipi, e dove l’ipotesi sbaglia
I tipi vengono dai caratteri dell’esempio e da nient’altro. Dove lo stesso nome porta valori dall’aspetto diverso l’ipotesi si allarga: forme intere mescolate danno xs:integer, un intero accanto a un decimale dà xs:decimal, tutto il resto ripiega su xs:string. Un attributo che alla seconda occorrenza sembra di un altro tipo scende a xs:string.
Il modo di rompersi segue da qui. Un campo di stato che contiene «1» e «2» viene tipizzato come intero, e lo schema rifiuta poi il «N/D» che quel campo porta una volta al mese. Un codice prodotto «0123» diventa un numero, cosa che perde lo zero iniziale e rende «0123» e «123» lo stesso valore. Una protezione va nella direzione opposta: cifre troppo lunghe per essere un intero sicuro, un numero di conto da 20 cifre per esempio, restano una stringa anziché diventare un numero che perde precisione.
- Un elemento vuoto dà xs:string, perché dal nulla non si deduce nulla.
- Sole cifre danno xs:nonNegativeInteger, oppure xs:integer con un segno meno davanti. Cifre con un punto decimale danno xs:decimal; la notazione scientifica ricade su xs:string.
- Esattamente «true» o «false» dà xs:boolean. «1», «yes» e «Y» no.
- AAAA-MM-GG dà xs:date, e lo stesso seguito da T e da un’ora dà xs:dateTime. Qualsiasi altro formato di data è una stringa.
- Un valore che inizia con http:// o https:// dà xs:anyURI. Altri schemi e i percorsi relativi no.
Ciò che un solo esempio non può dirti
L’occorrenza è la lacuna più grande. Un elemento viene marcato minOccurs="0" solo dove l’esempio lo ha mostrato assente: è comparso per la prima volta sotto un genitore successivo, oppure un genitore che lo aveva una volta è stato rivisto senza. Un campo facoltativo nel formato ma presente per tutto il tuo esempio esce come obbligatorio, e domani lo schema rifiuterà un documento lecito. Anche maxOccurs è grossolano: tutto ciò che è stato visto più di una volta sotto uno stesso genitore diventa unbounded, quindi una coppia che è sempre esattamente di due diventa illimitata.
L’ordine è affermato, non dedotto. xs:sequence dice che i figli devono comparire in quell’ordine, che è quello mostrato dall’esempio e che può non essere quello che il formato richiede. Se l’ordine non conta, usa xs:all, che XSD 1.0 consente solo in cima a un modello di contenuto e con ogni elemento al massimo una volta; se i figli sono alternative, usa xs:choice. Nulla deduce intervalli, enumerazioni, pattern, xs:key o xs:keyref, ed è lì che stanno le regole di business.
I namespace sono il limite più affilato. I nomi degli elementi sono presi esattamente come scritti, prefisso compreso, quindi un documento che contiene <dc:title> produce <xs:element name="dc:title">, e il nome di una dichiarazione di elemento deve essere un NCName, che non può contenere i due punti. Quello schema non compilerà.
La lista di revisione
Prima che uno schema generato si avvicini a una pipeline di build o a un partner, percorri questa lista. Comincia rimandando l’output nel validatore XSD contro lo stesso esempio: uno schema che non riesce a convalidare il documento da cui proviene è incappato in uno dei casi qui sopra.
- Ogni minOccurs. Quali campi sono davvero obbligatori, e quali erano soltanto presenti nel tuo esempio?
- Ogni maxOccurs="unbounded". Esiste un limite superiore reale?
- Ogni tipo, con più severità su codici, identificatori e stati usciti come interi o booleani.
- xs:sequence, e se non dovrebbe essere xs:all o xs:choice.
- targetNamespace e i nomi degli elementi, se l’esempio usava i namespace.
- mixed="true" su ogni elemento che porta testo accanto ai suoi figli.
- Ogni xs:element ref, che ha bisogno di una dichiarazione globale da puntare.
- I nomi fusi: un nome di elemento che significa due cose in due posti ha bisogno di due tipi.
- Ciò che nulla può dedurre: enumerazioni, pattern, intervalli, chiavi e riferimenti a chiavi.
Dedurre uno schema da codice
La deduzione di schemi non è in nessuna libreria standard tranne quella di .NET, quindi questi strumenti differiscono più del solito. Dai lo stesso documento a tutti e ottieni schemi diversi: le differenze stanno tutte nelle ipotesi.
// 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 capacità per cui vale la pena guardare altrove sono gli esempi multipli. Trang, XMLBeans e XmlSchemaInference accettano tutti più documenti e allargano il risultato, cosa che trasforma «questo campo era sempre presente» in «questo campo a volte manca» senza che tu debba tirare a indovinare.
Domande frequenti
Il mio documento di esempio viene caricato per generare lo schema?
No. La deduzione gira sullo scanner di questo sito, in JavaScript, in un Web Worker di questa scheda. Non c’è alcun server coinvolto e, a differenza del validatore di schemi, nemmeno un motore WebAssembly da scaricare.
Qui vale più di quanto sembri: un esempio è per definizione un payload reale, con dentro nomi di clienti, numeri di conto e prezzi veri.
Posso generare uno schema da più di un documento di esempio?
Qui no. Questa pagina deduce dal singolo documento presente nell’editor, che è il limite onesto di uno strumento costruito per rispondere in un incolla.
Più esempi producono davvero uno schema migliore, perché sono l’unico modo di scoprire che un campo è facoltativo. Trang accetta quanti file di input vuoi, l’Inst2Xsd di XMLBeans accetta un array di istanze, e XmlSchemaInference raffina uno schema esistente con un ulteriore esempio. Altrimenti genera dal tuo esempio più grande e allenta a mano i valori di minOccurs.
Perché il mio codice postale è uscito come intero?
Perché nell’esempio erano cifre, e nulla in un singolo documento distingue un numero da un codice che per caso è numerico.
È la cosa più comune da correggere in un output generato. Codici postali, codici prodotto, numeri di telefono, riferimenti di conto e qualsiasi cosa con uno zero iniziale dovrebbero quasi sempre essere xs:string, a volte con un xs:pattern.
Il mio documento usa i namespace e lo schema non compila. E adesso?
È previsto. I nomi degli elementi sono presi esattamente come scritti, quindi <dc:title> diventa <xs:element name="dc:title">, e il nome di una dichiarazione di elemento deve essere un NCName, che non può contenere i due punti.
Tratta l’output come un inventario strutturale. Aggiungi un targetNamespace all’elemento xs:schema e togli i prefissi dagli attributi name. elementFormDefault è scritto come «qualified»: i figli stanno nello stesso namespace del genitore.
L’output è XSD 1.0 o 1.1?
XSD 1.0, e nulla al suo interno usa un costrutto aggiunto dalla 1.1. È voluto: la 1.0 è ciò che libxml2, .NET, lxml e xmllint implementano tutti, mentre uno schema 1.1 funziona in Xerces-J, Saxon-EE e nel pacchetto Python xmlschema, e da nessun’altra parte.
Una regola fra campi come «la fine non deve precedere l’inizio» richiede xs:assert, che esiste solo nella 1.1. Aggiungile a mano, nella versione che i tuoi consumatori riescono a elaborare.
Come controllo se lo schema generato vale qualcosa?
Convalida l’esempio con esso, nel validatore XSD di questo sito. Uno schema che non riesce a convalidare la propria sorgente è incappato in uno dei casi noti: un nome di elemento con namespace, contenuto misto, o un ref che punta a una dichiarazione non globale.
Poi provalo su un documento che non ha mai visto, idealmente di un altro giorno o di un altro cliente. È lì che vengono a galla i valori sbagliati di minOccurs.