XML から XSD を生成
たたき台のスキーマを推論。信頼する前に必ず確認を。
すべてこのタブ内で実行されます。貼り付けた内容がアップロード・記録・送信されることはありません。 ネットワークパネルを開いて確認する.
サンプル文書を貼り付けると、それに対する XSD 1.0 の骨組みを作ります。要素名ごとに 1 つの宣言、子には xs:sequence、子か属性があるところには xs:complexType、テキストと属性の両方を持つ要素には xs:simpleContent、そしてすべての値に推測した型。このサイト自身のスキャナーが Web Worker の中で動くので、ダウンロードするエンジンはありません。
取引先がサンプルのペイロードだけ送ってきてスキーマをくれないとき、コードジェネレーターに食わせる何かが必要なとき、あるいは「相手のシステムが送ってくるもの」としてしか存在してこなかった形式の、書かれた説明が欲しいときに使ってください。
これが他と違うのは、知りえないことを自分から言う点です。1 つの文書からの推論は、その文書を説明するだけで、ほかの何も説明しません。どの要素が省略可能かも、実際の値の範囲も、形式が許す並び順も分かりません。出力はその警告をコメントとして抱えており、下の節はどの部分を見直すべきかを述べます。
実際に出力するもの
まず文書を走査し、整形式でなければ推論を断ります。それから各要素をたどって形を記録します。どの属性が現れ、それぞれが毎回現れたか、どの子が現れ、1 つの親の下にそれぞれいくつあったか、そしてテキストがどう見えたか。子も属性もない要素は型つきでその場に出力します。子を持つ要素には、サンプルが使った順のまま sequence を包む complexType を与えます。テキストと属性を持つ要素には、拡張つきの simpleContent を与えます。
細かい点が 3 つ効きます。形は経路ではなく要素名だけで索引されるので、<customer> の下の <name> と <product> の下の <name> は 1 つの宣言に統合されます。子を持つ要素が 2 回現れると、2 つめは <xs:element ref="..."/> と書かれますが、XSD はこれをグローバル宣言に対してしか解決しないので、宣言を持ち上げる必要があります。そして要素がテキストと子の両方を持つ場合、子が勝ってテキストは落とされます。混合内容には mixed="true" が要りますが、ここでは何もそれを付けません。
型はどう推測され、どこで推測が外れるか
型はサンプルの文字だけから来ます。同じ名前が違う見た目の値を持つ場合、推測は広がります。整数の形が混ざれば xs:integer、整数と小数が並べば xs:decimal、それ以外は xs:string に退きます。2 度目の出現で別の型に見える属性は xs:string に落ちます。
失敗の仕方もそこから従います。「1」と「2」を持つ状態フィールドは整数と型付けされ、するとスキーマは、そのフィールドが月に一度運ぶ「N/A」を拒みます。商品コード「0123」は数になり、先頭のゼロを失って「0123」と「123」が同じ値になります。逆向きの歯止めが 1 つあります。安全な整数にするには長すぎる数字列、たとえば 20 桁の口座番号は、精度を失う数になるのではなく文字列のままにします。
- 空の要素は xs:string になります。何もないところからは何も推論できないからです。
- 数字だけなら xs:nonNegativeInteger、先頭にマイナス記号があれば xs:integer。小数点つきの数字なら xs:decimal。指数表記は xs:string に落ちます。
- ちょうど「true」か「false」なら xs:boolean。「1」「yes」「Y」はそうなりません。
- YYYY-MM-DD は xs:date、同じものに T と時刻が続けば xs:dateTime。ほかの日付書式は文字列です。
- http:// か https:// で始まる値は xs:anyURI。ほかのスキームや相対パスはそうなりません。
サンプル 1 つでは分からないこと
いちばん大きな穴は出現数です。要素に minOccurs="0" が付くのは、サンプルがその不在を示した場所だけです。後の親の下で初めて現れた、あるいは一度それを持っていた親が、今度はそれなしで現れた、という場合です。形式としては省略可能でも、あなたのサンプルでは常に存在していたフィールドは必須として出てきて、明日スキーマが正当な文書を拒みます。maxOccurs も粗いものです。1 つの親の下で 2 回以上見えたものは unbounded になるので、いつもちょうど 2 つの対が無制限になってしまいます。
順序は推論ではなく主張です。xs:sequence は子がその順に現れなければならないと言いますが、それはサンプルが示したものであって、形式が求めるものとは限りません。順序が問題でなければ xs:all を使ってください。XSD 1.0 は内容モデルの最上位でのみ、各要素を最大 1 回として許します。子が選択肢なら xs:choice を使ってください。範囲、列挙、パターン、xs:key、xs:keyref はどれも推論されず、そしてそこに業務規則が宿っています。
名前空間がもっとも鋭い限界です。要素名は接頭辞も含めて書かれたとおりに取るので、<dc:title> を含む文書は <xs:element name="dc:title"> を生みますが、要素宣言の name は NCName でなければならず、NCName にコロンは入れられません。そのスキーマはコンパイルできません。
見直しの一覧
生成したスキーマをビルドのパイプラインや取引先に近づける前に、この一覧をたどってください。まず、出力を同じサンプルに対して XSD バリデーターに戻してみることです。自分が生まれた文書を検証できないスキーマは、上の場合のどれかに当たっています。
- すべての minOccurs。本当に必須のフィールドはどれで、あなたのサンプルに単に存在していただけのものはどれか。
- すべての maxOccurs="unbounded"。現実の上限はあるか。
- すべての型。整数や論理値として出てきたコード、識別子、状態にはいちばん厳しく。
- xs:sequence。それは xs:all や xs:choice にすべきではないか。
- サンプルが名前空間を使っていたなら、targetNamespace と要素名。
- 子と並べてテキストを持つ要素すべてに mixed="true"。
- すべての xs:element ref。指す先としてグローバル宣言が必要です。
- 統合された名前。2 か所で 2 つの意味を持つ要素名には、2 つの型が必要です。
- 何者も推論できないもの。列挙、パターン、範囲、キー、キー参照。
コードでスキーマを推論する
スキーマ推論は .NET のものを除いてどの標準ライブラリにも入っていないので、これらのツールはいつもより差が大きいです。同じ文書をすべてに与えれば、違うスキーマが返ってきます。その違いはすべて推測のところにあります。
// 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ほかを探す価値があるのは、複数サンプルへの対応です。Trang、XMLBeans、XmlSchemaInference はいずれも複数の文書を受け取って結果を広げてくれるので、「このフィールドは常に存在していた」が、あなたが当て推量をせずに「このフィールドはときどきない」に変わります。
よくある質問
スキーマを作るために、サンプル文書はアップロードされますか。
いいえ。推論はこのサイト自身のスキャナーで、JavaScript として、このタブの Web Worker の中で走ります。サーバーは関わりませんし、スキーマバリデーターと違って、ダウンロードする WebAssembly エンジンさえありません。
これは見た目より価値があります。サンプルとは定義上、実際のペイロードであり、実在の顧客名、口座番号、価格が入っているからです。
複数のサンプル文書からスキーマを作れますか。
ここではできません。このページはエディターにある 1 つの文書から推論します。それが、一度の貼り付けで答えるために作られた道具の正直な限界です。
サンプルが増えればよいスキーマになります。フィールドが省略可能だと学ぶ方法は、それしかないからです。Trang は入力ファイルをいくつでも取り、XMLBeans の Inst2Xsd はインスタンスの配列を取り、XmlSchemaInference は既存のスキーマをさらなるサンプルで洗練します。それ以外なら、いちばん大きなサンプルから生成して、minOccurs の値を手で緩めてください。
郵便番号が整数になってしまったのはなぜですか。
サンプルでは数字だったからです。そして 1 つの文書の中には、数と「たまたま数字から成るコード」を見分けるものが何もありません。
これは生成結果でいちばんよく直すことです。郵便番号、商品コード、電話番号、口座の参照番号、そして先頭にゼロが来るものはすべて、ほぼ常に xs:string にすべきで、ときには xs:pattern も添えるべきです。
文書が名前空間を使っていて、スキーマがコンパイルできません。どうすれば。
想定どおりです。要素名は書かれたとおりに取るので <dc:title> は <xs:element name="dc:title"> になりますが、要素宣言の name は NCName でなければならず、NCName にコロンは入れられません。
出力は構造の棚卸しとして扱ってください。xs:schema 要素に targetNamespace を足し、name 属性から接頭辞を取り除きます。elementFormDefault は「qualified」と書かれています。子は親と同じ名前空間にある、という意味です。
出力は XSD 1.0 ですか、1.1 ですか。
XSD 1.0 で、1.1 が追加した構文は何も使っていません。これは意図的です。1.0 は libxml2、.NET、lxml、xmllint がそろって実装しているもので、一方 1.1 のスキーマは Xerces-J、Saxon-EE、Python の xmlschema パッケージで動き、それ以外では動きません。
「終了は開始より前であってはならない」のようなフィールド横断の規則には xs:assert が必要で、これは 1.1 だけのものです。そうした規則は、受け取る側が処理できる版で、手で足してください。
生成されたスキーマが役に立つかどうか、どう確かめますか。
このサイトの XSD バリデーターで、サンプルをそのスキーマに照らして検証してください。自分の出どころを検証できないスキーマは、既知の場合のどれかに当たっています。名前空間つきの要素名、混合内容、あるいはグローバルでない宣言を指す ref です。
それから、そのスキーマが見たことのない文書、できれば別の日や別の顧客のものに当ててみてください。minOccurs の誤りが表に出るのはそこです。