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 の誤りが表に出るのはそこです。

関連ツール

関連する解説