XSD aus XML erzeugen
Leitet ein Ausgangsschema ab. Prüfen Sie es, bevor Sie ihm trauen.
Alles läuft in diesem Tab. Nichts, was Sie einfügen, wird hochgeladen, protokolliert oder irgendwohin gesendet. Öffnen Sie Ihr Netzwerkpanel und prüfen Sie es.
Fügen Sie ein Beispieldokument ein, und dies erzeugt dafür ein XSD-1.0-Gerüst: eine Deklaration je Elementname, xs:sequence für die Kinder, xs:complexType dort, wo es Kinder oder Attribute gibt, xs:simpleContent dort, wo ein Element Text und Attribute trägt, und einen geratenen Typ für jeden Wert. Es läuft auf dem eigenen Scanner dieser Seite in einem Web Worker, ohne eine Engine, die geladen werden müsste.
Man greift danach, wenn ein Partner Beispiel-Payloads und kein Schema geschickt hat, wenn Sie etwas brauchen, das Sie einem Codegenerator vorsetzen können, oder wenn Sie eine geschriebene Beschreibung eines Formats wollen, das bisher nur als „was das andere System eben schickt“ existierte.
Das Besondere hier ist, dass es Ihnen sagt, was es nicht wissen kann. Eine Ableitung aus einem Dokument beschreibt dieses Dokument und sonst nichts: nicht, welche Elemente optional sind, nicht die wirklichen Wertebereiche, nicht, welche Reihenfolgen das Format zulässt. Die Ausgabe trägt diese Warnung in einem Kommentar, und die Abschnitte unten sagen, welche Teile zu prüfen sind.
Was es tatsächlich ausgibt
Das Dokument wird zuerst gescannt, und die Ableitung wird verweigert, wenn es nicht wohlgeformt ist. Dann wird jedes Element besucht und eine Form festgehalten: welche Attribute auftraten und ob jedes jedes Mal auftrat, welche Kinder auftraten und wie viele von jedem unter einem Eltern-Element, und wie der Text aussah. Elemente ohne Kinder und ohne Attribute werden inline mit einem Typ ausgegeben; Elemente mit Kindern bekommen einen complexType, der eine sequence umschließt, in der Reihenfolge des Beispiels; Elemente mit Text und Attributen bekommen simpleContent mit einer Erweiterung.
Drei Details zählen. Formen werden allein über den Elementnamen indexiert, nicht über den Pfad, also verschmelzen ein <name> unter <customer> und ein <name> unter <product> zu einer Deklaration. Erscheint ein Element mit Kindern zweimal, wird das zweite als <xs:element ref="..."/> geschrieben, was XSD nur gegen eine globale Deklaration auflöst – Sie müssen sie also hochziehen. Und enthält ein Element Text und Kinder, gewinnen die Kinder und der Text fällt weg: Gemischter Inhalt braucht mixed="true", und das fügt hier nichts hinzu.
Wie Typen geraten werden, und wo das Raten schiefgeht
Typen kommen aus den Zeichen im Beispiel und aus nichts anderem. Wo derselbe Name unterschiedlich aussehende Werte trägt, weitet sich die Vermutung: gemischte Ganzzahlformen werden xs:integer, eine Ganzzahl neben einer Dezimalzahl wird xs:decimal, alles andere fällt auf xs:string zurück. Ein Attribut, das beim zweiten Auftreten nach einem anderen Typ aussieht, sinkt auf xs:string.
Die Fehlerweise folgt daraus. Ein Statusfeld mit „1“ und „2“ wird als Ganzzahl typisiert, und das Schema weist dann das „N/A“ ab, das dieses Feld einmal im Monat trägt. Ein Produktcode „0123“ wird eine Zahl, was die führende Null verliert und „0123“ und „123“ zum gleichen Wert macht. Eine Schutzregel läuft in die andere Richtung: Ziffern, die zu lang für eine sichere Ganzzahl sind, etwa eine 20-stellige Kontonummer, bleiben eine Zeichenkette, statt zu einer Zahl zu werden, die Genauigkeit verliert.
- Ein leeres Element gibt xs:string, denn aus nichts lässt sich nichts ableiten.
- Nur Ziffern geben xs:nonNegativeInteger, oder xs:integer mit führendem Minuszeichen. Ziffern mit Dezimalpunkt geben xs:decimal; wissenschaftliche Schreibweise fällt auf xs:string.
- Genau „true“ oder „false“ gibt xs:boolean. „1“, „yes“ und „Y“ nicht.
- JJJJ-MM-TT gibt xs:date, und dasselbe gefolgt von T und einer Zeit gibt xs:dateTime. Jedes andere Datumsformat ist eine Zeichenkette.
- Ein Wert, der mit http:// oder https:// beginnt, gibt xs:anyURI. Andere Schemata und relative Pfade nicht.
Was ein einzelnes Beispiel nicht sagen kann
Das Auftreten ist die größte Lücke. Ein Element wird nur dort mit minOccurs="0" markiert, wo das Beispiel es fehlen ließ: Es erschien erst unter einem späteren Eltern-Element, oder ein Eltern-Element, das es einmal hatte, wurde wieder ohne es gesehen. Ein Feld, das im Format optional ist, in Ihrem Beispiel aber durchgängig vorkommt, kommt als verpflichtend heraus, und das Schema wird morgen ein zulässiges Dokument abweisen. maxOccurs ist ebenfalls grob: Alles, was unter einem Eltern-Element mehr als einmal gesehen wurde, wird unbounded, also wird aus einem Paar, das immer genau zwei ist, etwas Unbegrenztes.
Die Reihenfolge wird behauptet, nicht abgeleitet. xs:sequence sagt, die Kinder müssten in dieser Reihenfolge erscheinen – das zeigte das Beispiel, und es muss nicht das sein, was das Format verlangt. Spielt die Reihenfolge keine Rolle, nehmen Sie xs:all, das XSD 1.0 nur an der Spitze eines Inhaltsmodells und mit jedem Element höchstens einmal erlaubt; sind die Kinder Alternativen, nehmen Sie xs:choice. Nichts leitet Bereiche, Aufzählungen, Muster, xs:key oder xs:keyref ab, und genau darin stecken die Geschäftsregeln.
Namensräume sind die schärfste Grenze. Elementnamen werden genau so genommen, wie sie geschrieben sind, samt Präfix, also ergibt ein Dokument mit <dc:title> ein <xs:element name="dc:title">, und der Name einer Elementdeklaration muss ein NCName sein, der keinen Doppelpunkt enthalten darf. Dieses Schema wird nicht übersetzen.
Die Prüfliste
Bevor ein erzeugtes Schema in die Nähe einer Build-Pipeline oder eines Partners kommt, gehen Sie diese Liste durch. Schicken Sie zuerst die Ausgabe im XSD-Validator gegen dasselbe Beispiel zurück: Ein Schema, das das Dokument nicht validieren kann, aus dem es stammt, ist auf einen der Fälle oben gestoßen.
- Jedes minOccurs. Welche Felder sind wirklich verpflichtend, und welche waren nur in Ihrem Beispiel vorhanden?
- Jedes maxOccurs="unbounded". Gibt es eine echte obere Grenze?
- Jeden Typ, am strengsten bei Codes, Kennungen und Status, die als Ganzzahlen oder Booleans herauskamen.
- xs:sequence, und ob es xs:all oder xs:choice sein sollte.
- targetNamespace und die Elementnamen, falls das Beispiel Namensräume benutzte.
- mixed="true" an jedem Element, das neben seinen Kindern Text trägt.
- Jedes xs:element ref, das eine globale Deklaration braucht, auf die es zeigen kann.
- Verschmolzene Namen: Ein Elementname, der an zwei Stellen zwei Dinge bedeutet, braucht zwei Typen.
- Was nichts ableiten kann: Aufzählungen, Muster, Bereiche, Schlüssel und Schlüsselverweise.
Ein Schema im Code ableiten
Schema-Ableitung steckt außer bei .NET in keiner Standardbibliothek, diese Werkzeuge gehen also weiter auseinander als üblich. Geben Sie allen dasselbe Dokument, und Sie bekommen verschiedene Schemata: Die Unterschiede liegen alle in den Vermutungen.
// 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.xmlDie Fähigkeit, derer es sich lohnt, anderswo nachzujagen, sind mehrere Beispiele. Trang, XMLBeans und XmlSchemaInference nehmen alle mehrere Dokumente und weiten das Ergebnis, was aus „dieses Feld war immer da“ ein „dieses Feld fehlt manchmal“ macht, ohne dass Sie raten müssen.
Häufige Fragen
Wird mein Beispieldokument hochgeladen, um das Schema zu erzeugen?
Nein. Die Ableitung läuft auf dem eigenen Scanner dieser Seite, in JavaScript, in einem Web Worker in diesem Tab. Es ist kein Server beteiligt und, anders als beim Schema-Validator, nicht einmal eine WebAssembly-Engine, die geladen werden müsste.
Das ist hier mehr wert, als es aussieht: Ein Beispiel ist per Definition eine echte Nutzlast, mit echten Kundennamen, Kontonummern und Preisen darin.
Kann ich ein Schema aus mehr als einem Beispieldokument erzeugen?
Hier nicht. Diese Seite leitet aus dem einen Dokument im Editor ab, und das ist die ehrliche Grenze eines Werkzeugs, das in einem Einfügen antworten soll.
Mehr Beispiele ergeben tatsächlich ein besseres Schema, denn sie sind der einzige Weg zu lernen, dass ein Feld optional ist. Trang nimmt beliebig viele Eingabedateien, Inst2Xsd von XMLBeans nimmt ein Array von Instanzen, und XmlSchemaInference verfeinert ein bestehendes Schema mit einem weiteren Beispiel. Sonst erzeugen Sie aus Ihrem größten Beispiel und lockern die minOccurs-Werte von Hand.
Warum kam meine Postleitzahl als Ganzzahl heraus?
Weil sie im Beispiel aus Ziffern bestand, und nichts in einem einzelnen Dokument eine Zahl von einem Code unterscheidet, der zufällig numerisch ist.
Das ist das Häufigste, was an erzeugter Ausgabe zu korrigieren ist. Postleitzahlen, Produktcodes, Telefonnummern, Kontoreferenzen und alles mit führender Null sollten fast immer xs:string sein, manchmal mit einem xs:pattern.
Mein Dokument benutzt Namensräume, und das Schema übersetzt nicht. Was nun?
Das ist zu erwarten. Elementnamen werden genau so genommen, wie sie geschrieben sind, also wird <dc:title> zu <xs:element name="dc:title">, und der Name einer Elementdeklaration muss ein NCName sein, der keinen Doppelpunkt enthalten darf.
Behandeln Sie die Ausgabe als strukturelles Inventar. Fügen Sie dem xs:schema-Element ein targetNamespace hinzu und entfernen Sie die Präfixe aus den name-Attributen. elementFormDefault wird als „qualified“ geschrieben: Kinder liegen im gleichen Namensraum wie ihr Eltern-Element.
Ist die Ausgabe XSD 1.0 oder 1.1?
XSD 1.0, und nichts darin benutzt ein Konstrukt, das 1.1 hinzufügte. Das ist Absicht: 1.0 ist, was libxml2, .NET, lxml und xmllint alle implementieren, während ein 1.1-Schema in Xerces-J, Saxon-EE und dem Python-Paket xmlschema funktioniert und sonst nirgends.
Eine feldübergreifende Regel wie „Ende darf nicht vor Start liegen“ braucht xs:assert, und das gibt es nur in 1.1. Fügen Sie solche von Hand hinzu, in der Version, die Ihre Abnehmer verarbeiten können.
Wie prüfe ich, ob das erzeugte Schema etwas taugt?
Validieren Sie das Beispiel dagegen, im XSD-Validator auf dieser Seite. Ein Schema, das seine eigene Quelle nicht validieren kann, ist auf einen der bekannten Fälle gestoßen: einen Elementnamen mit Namensraum, gemischten Inhalt, oder ein ref, das auf eine nicht globale Deklaration zeigt.
Probieren Sie es dann gegen ein Dokument, das es nie gesehen hat, am besten von einem anderen Tag oder einem anderen Kunden. Dort kommen die falschen minOccurs-Werte an die Oberfläche.