XSD バリデーター

XSD で検証。2 つのファイルはブラウザーから出ません。

入力
XSD スキーマ
待機中文書を貼り付けると検査します。入力中にそのまま検証されます。

すべてこのタブ内で実行されます。貼り付けた内容がアップロード・記録・送信されることはありません。 ネットワークパネルを開いて確認する.

左に文書を、右にその XSD を貼り付けて、「スキーマで検証」を押してください。WebAssembly にコンパイルした libxml2 が、このタブの Web Worker で検査を実行し、違反はすべて行・列とともに一覧になります。マーカーを押せばその位置へ飛べます。

ここへ来るのはたいてい、別の何かがその文書を拒否したあとです。取引先から cvc-complex-type.2.4.a が返ってきた、設定ファイルでビルドが落ちた、ゲートウェイが「ペイロードがスキーマに合わない」と言ってそこで止まった。手元に XSD はあり、欲しいのは失敗している要素であって、Java のツールチェーンではありません。

違いは実行場所です。ブラウザーにはスキーマバリデーターが組み込まれていないため、XSD 検証をうたう無料ツールは 2 つのファイルをサーバーへ送ります。最も有名なものは、まず XML を送信し、スキーマは 2 つめのフォームで送らせます。ここではどちらのファイルもこのタブから出ず、エンジン(gzip 後で約 480 KB)はページ読み込み時ではなくボタンを押したときに取得されます。

まず整形式、それから妥当性

これは 2 つの検査であって 1 つではありません。整形式は構文の話です。タグが閉じている、入れ子が正しい、ルートが 1 つ、アンパサンドがエスケープされている。文書だけあれば済むので入力しながら実行されます。妥当性は整形式に加えてスキーマへの適合であり、2 つめのファイルが要るので、意図して実行する操作にしてあります。

エンジンは XSD を解析し、コンパイルし、文書を解析し、それから検証します。各段階の失敗はそれぞれ性質が違います。整形式でない文書がスキーマにかけられることはありません。検査する筋の通った対象が存在しないからです。すべての診断には「スキーマの問題」か「妥当性エラー」のラベルが付き、スキーマ内の位置は意図的に押せません。その行番号はもう一方のペインのものだからです。

XSD 1.0 のみ。それで何が使えないか

libxml2 が実装しているのは XSD 1.0 です。XSD 1.1 は 2012 年 4 月 5 日に W3C 勧告となり、Apache Xerces-J、Saxon-EE、Python の xmlschema パッケージが実装していますが、libxml2 も、.NET の XmlSchemaSet も、libxml2 をラップする lxml も実装していません。1.1 の機能を使うスキーマはここではコンパイルできず、パネルは不可解な構造エラーではなく「1.1 が原因の可能性が高い」と示します。

  • xs:assert と xs:assertion ファセット:ある項目の妥当性が別の項目に依存する共起制約。XSD 1.0 ではそもそも表現できません。
  • xs:alternative(条件付き型割り当て):要素の型を、その要素自身の属性に対する XPath テストで選ぶ仕組み。
  • xs:openContent、xs:defaultOpenContent、xs:override、および 1.1 のデータ型 xs:dateTimeStamp、xs:dayTimeDuration、xs:yearMonthDuration。多くの記事に登場する xs:precisionDecimal は、最終勧告の前に削除されました。
  • さらに libxml2 固有の制限が 2 つ。xs:redefine は以前から未実装の経路に入りますし、xs:import はここでは schemaLocation を解決できません。このビルドはファイルを一切読めないからです。複数文書のスキーマ集合は、先に 1 つにまとめてください。

実際によく出る失敗

大半は順序が原因です。xs:sequence は子が宣言どおりの順序で現れることを要求します。この順序は書式の好みではなく契約の一部なので、たいていは要素を動かすのが正解です。XSD 1.0 で順序を問わない xs:all が使えるのは内容モデルの最上位だけで、各要素は高々 1 回まで。だから実務のスキーマはほとんどが sequence です。

残りは名前空間が原因です。スキーマが targetNamespace="urn:example:orders" を宣言していれば、それが支配するすべての要素はその名前空間に属していなければなりません。宣言がなければ、要素は名前空間なしでなければならず、既定の xmlns を足すと壊れます。関連する罠が elementFormDefault です。省略時は unqualified なので、ルートは修飾され、子は修飾されていてはいけません。

  • cvc-complex-type.2.4.a:スキーマが許していない場所に要素が現れました。波かっこ内の名前がそこで合法だったものなので、"One of {qty} is expected" は次に qty が来るはずだったという意味です。
  • cvc-complex-type.2.4.b:内容モデルを満たしていません。多くは末尾で必須の子が欠けています。
  • cvc-datatype-valid.1.2.1:テキストがその型として字句的に不正です。xs:int と宣言された空要素、秒のない dateTime、桁区切りの入った decimal など。
  • cvc-elt.1:ルート要素の宣言が見つかりません。ほぼ常に、宣言の欠落ではなく targetNamespace の不一致です。
  • cvc-complex-type.3.2.2:許されていない属性です。XMLSchema-instance 名前空間を宣言せずに xsi:type や xsi:nil を使っている場合がよくあります。

決してしないことと、その代償

xsi:schemaLocation を取得することは決してありません。XSD Part 1 はこの属性を「スキーマ文書の物理的位置についての」ヒントと呼び、プロセッサーは「たとえば呼び出し側アプリケーションから別途指示されない限り、デリファレンスを試みるべきである」と述べています。取得しないことは仕様に適合していますし、よくあることでもあります。SQL Server は xml 型の列でこれを無視します。

そもそも取得したくてもできません。すべての解析は XML_PARSE_NO_XXE、NONET、NO_SYS_CATALOG を使い、このビルドはリソースローダーを登録していないので、外部実体も外部 DTD サブセットも schemaLocation の URL も、すべて何にも解決しません。XML_PARSE_HUGE は決して設定しないため、libxml2 の上限が有効なままです。要素の深さ 256、実体の入れ子 20、そして増幅の上限。

正直に言っておくべき弱点が 1 つ。libxml2 の文書は自身のスキーマモジュールを XML Schema Part 1 の部分的な実装と呼んでおり、同じ入力に対して Xerces より診断が少ないことがよくあります。一覧は「少なくともこれだけ」と読み、1 つ直すたびに再実行してください。

コードで XSD 検証する

XML を扱う言語での同じ検査です。どのサンプルも外部アクセスを無効にしています。スキーマ参照とは取得の指示であり、これらのライブラリの多くは既定でそれに従ってしまうからです。

// npm i libxml2-wasm, the same engine this page runs.
import { XmlDocument, XsdValidator, ParseOption, XmlValidateError } from 'libxml2-wasm';

// NO_XXE blocks external DTDs and entities. Leaving HUGE unset is what keeps
// libxml2's depth, entity-nesting and amplification limits switched on.
const SAFE = ParseOption.XML_PARSE_NO_XXE | ParseOption.XML_PARSE_NONET;

export function validateXsd(xmlText, xsdText) {
  let xsd = null;
  let validator = null;
  let doc = null;
  try {
    xsd = XmlDocument.fromString(xsdText, { option: SAFE });
    validator = XsdValidator.fromDoc(xsd);
    doc = XmlDocument.fromString(xmlText, { option: SAFE });
    validator.validate(doc);
    return { valid: true, errors: [] };
  } catch (err) {
    if (err instanceof XmlValidateError) {
      // Each detail is { line, col, level, message, xpath }.
      return { valid: false, errors: err.details };
    }
    throw err;
  } finally {
    // dispose() is mandatory: these are WebAssembly heap allocations.
    doc?.dispose();
    validator?.dispose();
    xsd?.dispose();
  }
}
# pip install lxml
from lxml import etree

parser = etree.XMLParser(
    resolve_entities=False,   # no entity substitution
    no_network=True,          # never fetch a schema or DTD
    load_dtd=False,
    huge_tree=False,          # keep the depth and expansion limits
)

schema = etree.XMLSchema(etree.parse('schema.xsd', parser))
doc = etree.parse('document.xml', parser)

if schema.validate(doc):
    print('valid')
else:
    for e in schema.error_log:
        print(f'{e.line}:{e.column} {e.message}')

# lxml wraps libxml2, so this is XSD 1.0. For xs:assert and xs:alternative use
# the pure-Python implementation instead:
#   import xmlschema
#   xmlschema.XMLSchema11('schema.xsd').validate('document.xml')
import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
// With Xerces on the classpath, XSD 1.1 is one string away:
//   SchemaFactory.newInstance("http://www.w3.org/XML/XMLSchema/v1.1");

// "file" allows xs:import of a schema next to this one; http is refused.
// Pass "" instead to block every external reference, local ones included.
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file");
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");

Schema schema = factory.newSchema(new StreamSource(new java.io.File("schema.xsd")));
Validator validator = schema.newValidator();
validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");

// Without an ErrorHandler the first error throws and you see one problem.
// With one, Xerces carries on and reports the lot.
validator.setErrorHandler(new DefaultHandler() {
    @Override public void error(SAXParseException e) {
        System.out.printf("%d:%d %s%n", e.getLineNumber(), e.getColumnNumber(), e.getMessage());
    }
    @Override public void fatalError(SAXParseException e) throws SAXParseException {
        throw e;
    }
});

validator.validate(new StreamSource(new java.io.File("document.xml")));
using System;
using System.Xml;
using System.Xml.Schema;

var schemaSettings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null,          // no xs:import over the network
};

var schemas = new XmlSchemaSet { XmlResolver = null };
using (var schemaReader = XmlReader.Create("schema.xsd", schemaSettings))
{
    schemas.Add(null, schemaReader);   // null: take targetNamespace from the file
}

var settings = new XmlReaderSettings
{
    ValidationType = ValidationType.Schema,
    Schemas = schemas,
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null,
};
// Never follow xsi:schemaLocation from the instance document.
settings.ValidationFlags &= ~XmlSchemaValidationFlags.ProcessSchemaLocation;
// Without a handler the first error throws; with one, validation continues.
settings.ValidationEventHandler += (_, e) =>
    Console.WriteLine($"{e.Exception.LineNumber}:{e.Exception.LinePosition} {e.Message}");

using var reader = XmlReader.Create("document.xml", settings);
while (reader.Read()) { }    // validation happens as the document is read

// System.Xml.Schema is XSD 1.0, and Microsoft has said it is not moving past it.
<?php
// DOMDocument is libxml2, so this is the same engine and the same XSD 1.0.
libxml_use_internal_errors(true);

$doc = new DOMDocument();
if (!$doc->loadXML($source, LIBXML_NONET)) {
    fwrite(STDERR, "The document is not well-formed.\n");
    exit(1);
}

// schemaValidateSource() takes the XSD as a string if you already have it.
if (!$doc->schemaValidate('schema.xsd')) {
    foreach (libxml_get_errors() as $e) {
        fprintf(STDERR, "%d:%d %s\n", $e->line, $e->column, trim($e->message));
    }
    libxml_clear_errors();
    exit(1);
}

echo "valid\n";
# xmllint ships with libxml2 and is the same engine as this page.
# --nonet stops it fetching anything the document or the schema references.
xmllint --noout --nonet --schema schema.xsd document.xml

# Exit status is 0 when the document is valid, non-zero otherwise, and the
# diagnostics go to stderr in file:line: form.

# A schema split across xs:import files works here, because xmllint reads the
# sibling files from disk. That is the one thing a browser cannot do.

# No widely installed command-line tool does XSD 1.1; for that you need
# Xerces-J or Saxon-EE, driven from code.

Java と C# のサンプルにあるハンドラーの型に注目してください。どちらの API も、ハンドラーを設定しない限り最初のエラーで例外を投げます。実運用のコードが一度に 1 件しか報告しないのは、たいていこれが理由です。

よくある質問

XML と XSD はどこかにアップロードされますか。

いいえ。両方のペインともこのタブに留まります。libxml2 はここで WebAssembly として Web Worker 内で動き、アップロード用のエンドポイントはなく、このビルドは自前のネットワーク経路を持ちません。

このページではその点が他のページ以上に重要です。スキーマはすべての項目、コードリスト、内部の境界に名前を与えるものであり、文書のほうがテストデータにすぎない場合でもスキーマは秘密保持契約下にあることが珍しくありません。ネットワークパネルを開いて「検証」を押し、エンジンが一度読み込まれたあと何も起きないのを確かめてください。両方のペインは再読み込みで作業が消えないよう localStorage に保存され、300 KB を超えるものは保存されません。

XSD 1.1 や xs:assert に対応していますか。

していません。libxml2 は XSD 1.0 なので、xs:assert、xs:alternative、xs:openContent、xs:override はコンパイルできません。不可解な「element assert is not expected here」ではなく、スキーマは整形式の XML だが使える XSD ではない旨と、1.1 が原因の可能性が高いことをパネルが伝えます。

1.1 が必要なら、名前空間文字列を 1 つ変えるだけで 1.1 の SchemaFactory が得られる無料の Xerces-J、Saxon-EE、Python の xmlschema パッケージを使ってください。ブラウザーベースのもので対応しているものはありません。どれも libxml2 のビルドだからです。

スキーマが xs:import を使っています。ここで検証できますか。

先に 1 つにまとめた場合だけです。xs:import の解決とはファイルを読むかリクエストを出すことであり、このビルドはリソースローダーを登録していません。外部実体を取得しないのと同じ判断であり、そのためスキーマはコンパイルできません。

インポートされている型定義を、検証に使う文書へ貼り込むか、ローカルで xmllint を実行してください。ローカルなら隣の .xsd ファイルはディスクから読めます。UBL や FpML をはじめ、大きな公開スキーマ群の多くはこの扱いが必要です。

cvc-complex-type.2.4.a はどういう意味ですか。

スキーマが許していない場所に要素が現れた、という意味です。メッセージの末尾には、そこで合法だった名前が波かっこで並びます。"Invalid content was found starting with element 'item'. One of '{qty}' is expected." なら、次に来るべきは qty でした。

原因はほぼ 3 つに尽きます。xs:sequence に対して子の順序が違う(圧倒的に多い)。要素がそもそも宣言されていない(たいていは打ち間違いか、連携の片側が足した項目)。名前は合っていて名前空間が違う。最後の 1 つはメッセージからは分かりません。ローカル名しか印字されないからです。

xsi:schemaLocation に書かれたスキーマを取得しますか。

決してしません。仕様はこの属性をヒントと呼び、呼び出し側アプリケーションがデリファレンスしないよう指示できると定めています。ですから取得を断るのは近道ではなく、仕様に適合した振る舞いです。

理由はプライバシーだけではありません。信頼できない文書に書かれた攻撃者制御の URL をたどることはリクエストフォージェリの足がかりですし、検証結果が「今日そのサードパーティが配っているもの」に左右されることにもなります。

どのくらいの大きさの文書を検査できますか。

20 メガバイトです。それ以上のファイルの読み込みは断ります。すべてがこのページのメモリー上にあり、検証は完全な木に加えてコンパイル済みスキーマも構築するからです。

本当に大きな文書には、ローカルで xmllint を実行してください。同じ libxml2 のコードを、ブラウザーの上限なしで使えます。

関連ツール

関連する解説

これで解決できるエラー