Générer un XSD à partir de XML
Déduit un schéma de départ. Relisez-le avant de vous y fier.
Tout s'exécute dans cet onglet. Rien de ce que vous collez n'est envoyé, journalisé ni transmis où que ce soit. Ouvrez votre panneau réseau et vérifiez.
Collez un document d’exemple et ceci en produit une ossature XSD 1.0 : une déclaration par nom d’élément, xs:sequence pour les enfants, xs:complexType là où il y a des enfants ou des attributs, xs:simpleContent là où un élément porte à la fois du texte et des attributs, et un type deviné pour chaque valeur. Cela tourne sur l’analyseur propre à ce site, dans un Web Worker, sans aucun moteur à télécharger.
On y a recours quand un partenaire a envoyé des charges d’exemple et aucun schéma, quand il vous faut quelque chose à donner à un générateur de code, ou quand vous voulez une description écrite d’un format qui n’a existé jusqu’ici que comme « ce que l’autre système envoie ».
Ce qui distingue celui-ci, c’est qu’il vous dit ce qu’il ne peut pas savoir. Inférer depuis un seul document décrit ce document et rien d’autre : ni quels éléments sont facultatifs, ni les plages de valeurs réelles, ni quels ordres le format autorise. La sortie porte cet avertissement dans un commentaire, et les sections ci-dessous disent quelles parties relire.
Ce qu’il produit réellement
Le document est d’abord analysé, et l’inférence est refusée s’il n’est pas bien formé. Puis chaque élément est visité et une forme enregistrée : quels attributs sont apparus et si chacun est apparu chaque fois, quels enfants sont apparus et combien de chacun sous un même parent, et à quoi ressemblait le texte. Les éléments sans enfants ni attributs sont émis en ligne avec un type ; les éléments à enfants reçoivent un complexType enveloppant une sequence, dans l’ordre qu’a montré l’exemple ; les éléments à texte et attributs reçoivent un simpleContent avec une extension.
Trois détails comptent. Les formes sont indexées sur le seul nom d’élément, pas sur le chemin, donc un <name> sous <customer> et un <name> sous <product> fusionnent en une seule déclaration. Quand un élément à enfants apparaît deux fois, le second est écrit <xs:element ref="..."/>, que XSD ne résout que contre une déclaration globale : il faut donc la remonter. Et si un élément contient à la fois du texte et des enfants, les enfants gagnent et le texte est perdu : le contenu mixte exige mixed="true", que rien ici n’ajoute.
Comment les types sont devinés, et où la devinette se trompe
Les types viennent des caractères de l’exemple et de rien d’autre. Là où le même nom porte des valeurs d’aspect différent, la devinette s’élargit : des formes entières mêlées donnent xs:integer, un entier à côté d’un décimal donne xs:decimal, tout le reste retombe sur xs:string. Un attribut qui paraît d’un autre type à sa seconde occurrence descend à xs:string.
Le mode de défaillance s’ensuit. Un champ d’état contenant « 1 » et « 2 » est typé en entier, et le schéma rejette alors le « N/A » que ce champ porte une fois par mois. Un code produit « 0123 » devient un nombre, ce qui perd le zéro de tête et rend « 0123 » et « 123 » identiques. Un garde-fou va dans l’autre sens : des chiffres trop longs pour être un entier sûr, un numéro de compte à 20 chiffres par exemple, restent une chaîne plutôt que de devenir un nombre qui perd de la précision.
- Un élément vide donne xs:string, parce qu’on ne peut rien inférer de rien.
- Des chiffres seuls donnent xs:nonNegativeInteger, ou xs:integer avec un signe moins devant. Des chiffres avec une virgule décimale donnent xs:decimal ; la notation scientifique retombe sur xs:string.
- Exactement « true » ou « false » donne xs:boolean. « 1 », « yes » et « Y » non.
- AAAA-MM-JJ donne xs:date, et la même chose suivie d’un T et d’une heure donne xs:dateTime. Tout autre format de date est une chaîne.
- Une valeur commençant par http:// ou https:// donne xs:anyURI. Les autres schémas et les chemins relatifs non.
Ce qu’un seul exemple ne peut pas vous dire
L’occurrence est la plus grande lacune. Un élément est marqué minOccurs="0" seulement là où l’exemple l’a montré absent : il est apparu pour la première fois sous un parent plus tardif, ou un parent qui l’avait une fois a été revu sans lui. Un champ facultatif dans le format mais présent partout dans votre exemple sort obligatoire, et le schéma rejettera demain un document licite. maxOccurs est grossier aussi : tout ce qui a été vu plus d’une fois sous un même parent devient unbounded, donc une paire toujours exactement double devient illimitée.
L’ordre est affirmé, non inféré. xs:sequence dit que les enfants doivent apparaître dans cet ordre, ce qu’a montré l’exemple et qui n’est pas forcément ce que le format exige. Si l’ordre n’importe pas, utilisez xs:all, que XSD 1.0 n’autorise qu’au sommet d’un modèle de contenu et avec chaque élément au plus une fois ; si les enfants sont des alternatives, utilisez xs:choice. Rien n’infère les plages, les énumérations, les motifs, xs:key ou xs:keyref, et ce sont eux qui portent les règles métier.
Les espaces de noms sont la limite la plus tranchante. Les noms d’éléments sont pris exactement comme écrits, préfixe compris, donc un document contenant <dc:title> produit <xs:element name="dc:title">, et le nom d’une déclaration d’élément doit être un NCName, qui ne peut pas contenir de deux-points. Ce schéma ne compilera pas.
La liste de relecture
Avant qu’un schéma généré n’approche d’une chaîne de build ou d’un partenaire, parcourez cette liste. Commencez par repasser la sortie dans le validateur XSD contre le même exemple : un schéma incapable de valider le document dont il est issu est tombé sur l’un des cas ci-dessus.
- Chaque minOccurs. Quels champs sont réellement obligatoires, et lesquels étaient simplement présents dans votre exemple ?
- Chaque maxOccurs="unbounded". Existe-t-il une borne supérieure réelle ?
- Chaque type, en étant le plus dur sur les codes, identifiants et états sortis en entiers ou en booléens.
- xs:sequence, et s’il ne devrait pas être xs:all ou xs:choice.
- targetNamespace et les noms d’éléments, si l’exemple utilisait des espaces de noms.
- mixed="true" sur tout élément portant du texte aux côtés de ses enfants.
- Tout xs:element ref, qui a besoin d’une déclaration globale à viser.
- Les noms fusionnés : un nom d’élément signifiant deux choses à deux endroits a besoin de deux types.
- Ce que rien ne peut inférer : énumérations, motifs, plages, clés et références de clés.
Inférer un schéma en code
L’inférence de schéma n’est dans aucune bibliothèque standard sauf celle de .NET, donc ces outils diffèrent plus que d’habitude. Donnez le même document à tous et vous obtenez des schémas différents : les écarts sont tous dans les devinettes.
// 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é qui vaut d’être cherchée ailleurs, ce sont les exemples multiples. Trang, XMLBeans et XmlSchemaInference acceptent tous plusieurs documents et élargissent le résultat, ce qui transforme « ce champ était toujours présent » en « ce champ est parfois absent » sans que vous ayez à le deviner.
Questions fréquentes
Mon document d’exemple est-il téléversé pour générer le schéma ?
Non. L’inférence tourne sur l’analyseur propre à ce site, en JavaScript, dans un Web Worker de cet onglet. Aucun serveur n’est impliqué et, contrairement au validateur de schéma, il n’y a même pas de moteur WebAssembly à télécharger.
Cela vaut plus ici qu’il n’y paraît : un exemple est par définition une charge réelle, avec de vrais noms de clients, numéros de compte et prix dedans.
Puis-je générer un schéma à partir de plusieurs documents d’exemple ?
Pas ici. Cette page infère depuis le seul document présent dans l’éditeur, ce qui est la limite honnête d’un outil conçu pour répondre en un collage.
Plus d’exemples donnent bel et bien un meilleur schéma, car c’est le seul moyen d’apprendre qu’un champ est facultatif. Trang prend autant de fichiers d’entrée que vous voulez, l’Inst2Xsd de XMLBeans prend un tableau d’instances, et XmlSchemaInference affine un schéma existant avec un exemple supplémentaire. Sinon, générez depuis votre plus gros exemple et relâchez les valeurs de minOccurs à la main.
Pourquoi mon code postal est-il sorti en entier ?
Parce que dans l’exemple c’étaient des chiffres, et que rien dans un seul document ne distingue un nombre d’un code qui se trouve être numérique.
C’est la chose la plus fréquente à corriger dans une sortie générée. Codes postaux, codes produits, numéros de téléphone, références de compte et tout ce qui porte un zéro de tête devraient presque toujours être xs:string, parfois avec un xs:pattern.
Mon document utilise des espaces de noms et le schéma ne compile pas. Et maintenant ?
C’est attendu. Les noms d’éléments sont pris exactement comme écrits, donc <dc:title> devient <xs:element name="dc:title">, et le nom d’une déclaration d’élément doit être un NCName, qui ne peut pas contenir de deux-points.
Traitez la sortie comme un inventaire structurel. Ajoutez un targetNamespace à l’élément xs:schema et retirez les préfixes des attributs name. elementFormDefault est écrit « qualified » : les enfants sont dans le même espace de noms que leur parent.
La sortie est-elle du XSD 1.0 ou 1.1 ?
Du XSD 1.0, et rien dedans n’utilise une construction ajoutée par 1.1. C’est délibéré : 1.0 est ce qu’implémentent libxml2, .NET, lxml et xmllint, tandis qu’un schéma 1.1 marche dans Xerces-J, Saxon-EE et le paquet Python xmlschema, et nulle part ailleurs.
Une règle entre champs comme « la fin ne doit pas précéder le début » exige xs:assert, qui n’existe qu’en 1.1. Ajoutez-les à la main, dans la version que vos consommateurs savent traiter.
Comment vérifier que le schéma généré vaut quelque chose ?
Validez l’exemple contre lui, dans le validateur XSD de ce site. Un schéma incapable de valider sa propre source est tombé sur l’un des cas connus : un nom d’élément avec espace de noms, du contenu mixte, ou un ref visant une déclaration qui n’est pas globale.
Essayez-le ensuite contre un document qu’il n’a jamais vu, idéalement d’un autre jour ou d’un autre client. C’est là que les mauvaises valeurs de minOccurs remontent.