Java で XML が XSD スキーマ構造を満たしているかバリデーションする

Java のクラスライブラリ javax.xml.validation を使った XML スキーマ検証をします。スキーマエラーを全て取得する方法も紹介しています。

やりたいこと

銀行間の国際的な決済メッセージングネットワークである SWIFT のメッセージフォーマットが ISO 20022 に準拠するということで、金融機関界隈では XML 周りの処理についてよく聞かれます。今回は対象の XML が XSD で定められているスキーマ構造を満たしているか検証し、満たしていない場合はその内容を取得するということをやってみたいと思います。

XML のスキーマ検証は C ベースの XML ツールである libxml2 (GitHub - GNOME/libxml2: Read-only mirror of https://gitlab.gnome.org/GNOME/libxml2)や Java のクラスライブラリ javax.xml.validation が 2 強で、他言語のライブラリもこれらをラップしたものであることが多いです。ここでは Java のクラスライブラリ javax.xml.validation を使用して XML のスキーマ検証をやってみます。

環境

Java
Oracle Java SE 22

準備

検証の元となるスキーマ定義ファイル (xsd) と検証される XML ファイルを用意します。 具体的に検証したい XML や XSD が手持ちである場合はそれを使ってください。

まずスキーマ定義ファイルですが、ISO 20022 Message Definitions | ISO20022から CustomerCreditTransferInitiationV12 の xsd ファイル pain.001.001.12.xsd をダウンロードします。

次に検証される XML ファイルとして、Sample pain.001.001.xxx Files - Goldman Sachs Developer から US Fedwire v3 をコピーし pain.001.001.03.xml として保存します。

簡単な XML スキーマ検証

簡単な XML スキーマ検証の例

XML スキーマ検証をする最小限のコードとしては以下のようになります。準備した各ファイルは同ディレクトリに配置し、ファイル名をハードコードする形にしています。

XMLValidation.java
import java.io.File;
import java.io.IOException;

import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.xml.sax.SAXException;

public class XMLValidation {

  public static void main(String[] args) {
    try {
      Validator validator = SchemaFactory
        .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
        .newSchema(new File("pain.001.001.12.xsd"))
        .newValidator();
      validator.validate(new StreamSource(new File("pain.001.001.03.xml")));
    } catch (IOException | SAXException e) {
      System.out.println(e.getMessage());
    }
  }
}

少し長くなりますが一つ一つ見ていきます。

SchemaFactory.newInstance() メソッドで SchemaFactory のインスタンスを作成します。SchemaFactory.newInstance() の引数はスキーマ言語の名前空間 URI を渡します。

public static SchemaFactory newInstance(String schemaLanguage)

Obtains a new instance of a SchemaFactory that supports the specified schema language. - SchemaFactory (Java SE 22 & JDK 22)

通常スキーマ言語の名前空間 URI は W3C の XML Schema 1.0 で http://www.w3.org/2001/XMLSchema です。先ほど準備した pain.001.001.12.xsd を見てみると xmlns:xs="http://www.w3.org/2001/XMLSchema" と名前空間が定義されており xs: の名前空間プレフィクス付き要素でスキーマ構造が定義されていますので、W3C の XML Schema 1.0 でスキーマが定義されていることがわかります。

W3C の XML Schema 1.0 の名前空間 URI は XMLConstants.W3C_XML_SCHEMA_NS_URI で定義されているのでこちらを使用します。

public static final String W3C_XML_SCHEMA_NS_URI

W3C XML Schema Namespace URI. Defined to be “http://www.w3.org/2001/XMLSchema”.
XMLConstants (Java SE 22 & JDK 22)

次に SchemaFactory.newSchema() メソッドにスキーマ定義を渡して Schema インスタンスを作成します。

public Schema newSchema(File schema)
               throws SAXException

Parses the specified File as a schema and returns it as a Schema.
SchemaFactory (Java SE 22 & JDK 22)

さらに Schema.newValidator() メソッドで Validator インスタンスを作成します。

public abstract Validator newValidator()

Creates a new Validator for this Schema. - Schema (Java SE 22 & JDK 22)

これで pain.001.001.12.xsd のスキーマに沿った Validator インスタンスが作成できました。

最後に Validator.validate() メソッドで XML pain.001.001.03.xml の検証を行います。

public void validate(Source source)
              throws SAXException,
              IOException

Validates the specified input. Validator (Java SE 22 & JDK 22)

Validator.validate() メソッドは File を直接受け取ってはくれないので StreamSource クラスを介して XML ファイルを渡しています。

public StreamSource(File f)

Construct a StreamSource from a File. - StreamSource (Java SE 22 & JDK 22)

Validator.validation() メソッドはスキーマ検証でエラーが発生した場合、SAXException を投げるのでそれを catch して出力する形にしています。

実行およびスキーマ検証結果

ではクラスコンパイルして実行してみます。

javac XMLValidation.java
java XMLValidation

すると以下エラーが出力されます。

cvc-elt.1.a: 要素'Document'の宣言が見つかりません。

pain.001.001.12.xsdpain.001.001.03.xml のファイル名の最後の 1203 はバージョン番号なのですが、XML 名前空間がしっかりバージョン番号まで含めて切られているため、名前空間 urn:iso:std:iso:20022:tech:xsd:pain.001.001.03Document はあるけど、名前空間 urn:iso:std:iso:20022:tech:xsd:pain.001.001.12Document は見つからないと言っています。

まあこれも立派な検証ではあるのですが、ちょっとつまらないので pain.001.001.03.xml を編集して名前空間を urn:iso:std:iso:20022:tech:xsd:pain.001.001.03 から urn:iso:std:iso:20022:tech:xsd:pain.001.001.12 に変更して再度実行してみましょう。

すると以下のエラーが出ます。

cvc-complex-type.2.3: タイプのコンテンツ・タイプが要素のみであるため、要素'ReqdExctnDt'には文字[children]を使用できません。

pain.001.001.03.xml の該当箇所は以下です。

pain.001.001.03.xml
<ReqdExctnDt>2024-05-10</ReqdExctnDt>

一方 pain.001.001.12.xsd では ReqdExctnDt 要素の定義は以下となっていて、DateAndDateTime2Choice 型である必要があります。

pain.001.001.12.xsd
<xs:element name="ReqdExctnDt" type="DateAndDateTime2Choice"/>

DateAndDateTime2Choice 型の定義は以下となっていて Dt 要素か DtTm 要素である必要があります。

pain.001.001.12.xsd
<xs:complexType name="DateAndDateTime2Choice">
  <xs:choice>
    <xs:element name="Dt" type="ISODate"/>
    <xs:element name="DtTm" type="ISODateTime"/>
  </xs:choice>
</xs:complexType>

従って、pain.001.001.03.xml の該当箇所を以下のように修正するとエラーは解消されます。

pain.001.001.03.xml
<ReqdExctnDt>
  <Dt>2024-05-10</Dt>
</ReqdExctnDt>

上記修正後、再度実行するとまた別のエラーがでますが、繰り返し XML スキーマの話になるのでここでは解説しません。

全てのスキーマ検証結果を取得

ここまで見てきたように現状のコードではスキーマ検証エラーが一つずつしか出力されないので、XML を修正しては再度実行してと繰り返しになります。これはいけてないので全てのスキーマ検証結果を一度で取得できるようにしたいと思います。

エラーハンドラー

ValidatorsetErrorHandler() メソッドで ErrorHandler を設定し、エラー発生時の挙動を設定することができます。

public abstract void setErrorHandler(ErrorHandler errorHandler)

Sets the ErrorHandler to receive errors encountered during the validate method invocation.
Error handler can be used to customize the error handling process during a validation. When an ErrorHandler is set, errors found during the validation will be first sent to the ErrorHandler.
Validator (Java SE 22 & JDK 22)

なお ErrorHandler が未設定の場合、以下の通り即時 throw する ErrorHandler が設定されるので、一つエラーが検知されると止まってしまっていたわけです。

When the ErrorHandler is null, the implementation will behave as if the following ErrorHandler is set:

class DraconianErrorHandler implements ErrorHandler {
  public void fatalError( SAXParseException e ) throws SAXException {
    throw e;
  }
  public void error( SAXParseException e ) throws SAXException {
    throw e;
  }
  public void warning( SAXParseException e ) throws SAXException {
    // noop
  }
}

Validator (Java SE 22 & JDK 22)

エラーハンドラーを作成

それではカスタマイズしたエラーハンドラーを作成し、すべてのスキーマ検証エラーを取得してみましょう。カスタマイズしたエラーハンドラーは以下の ErrorHandler インターフェースを実装する必要があります。

public interface ErrorHandler

Basic interface for SAX error handlers.
If a SAX application needs to implement customized error handling, it must implement this interface and then register an instance with the XML reader using the setErrorHandler method. The parser will then report all errors and warnings through this interface. - ErrorHandler (Java SE 22 & JDK 22)

実際に実装してみた例がこちらです。スキーマ検証エラーが発生した場合、throw するのではなく ArrayList にエラーをためていくようにしています。

SchemaErrorHandler.java
import java.util.ArrayList;
import java.util.List;

import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXParseException;

public class SchemaErrorHandler implements ErrorHandler {

  private List<SAXParseException> exceptions;

  public SchemaErrorHandler() {
    this.exceptions = new ArrayList<>();
  }

  public List<SAXParseException> getExceptions() {
    return exceptions;
  }

  @Override
  public void warning(SAXParseException exception) {
    exceptions.add(exception);
  }

  @Override
  public void error(SAXParseException exception) {
    exceptions.add(exception);
  }

  @Override
  public void fatalError(SAXParseException exception) {
    exceptions.add(exception);
  }
}

エラーハンドラーを Validator にセットする

カスタマイズしたエラーハンドラーを使用して元のコードを書き換えてみます。Validator.setErrorHandler() メソッドでカスタマイズしたエラーハンドラーをセットし、最後にためこんだエラーをすべて出力させています。

XMLValidation.java
import java.io.File;
import java.io.IOException;

import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.xml.sax.SAXException;

public class XMLValidation {

  public static void main(String[] args) {
    SchemaErrorHandler errorHandler = new SchemaErrorHandler();

    try {
      Validator validator = SchemaFactory
        .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
        .newSchema(new File("pain.001.001.12.xsd"))
        .newValidator();
      validator.setErrorHandler(errorHandler);
      validator.validate(new StreamSource(new File("pain.001.001.3.xml")));
    } catch (IOException | SAXException e) {
      System.out.println(e.getMessage());
    }

    errorHandler.getExceptions().forEach(e -> System.out.println(
      String.format("Ln: %s, Col: %s. %s", e.getLineNumber(), e.getColumnNumber(), e.getMessage())
      ));
  }
}

実行およびスキーマ検証結果

再度クラスコンパイルして実行してみます。

javac XMLValidation.java
java XMLValidation

以下の通り全ての検証エラーを一度の実行で取得することができました。

Ln: 22, Col: 50. cvc-complex-type.2.3: タイプのコンテンツ・タイプが要素のみであるため、要素'ReqdExctnDt'には文字[children]を使用できません。
Ln: 22, Col: 50. cvc-complex-type.2.4.b: 要素'ReqdExctnDt'のコンテンツは不完全です。'{"urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":Dt, "urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":DtTm}'のいずれかが必要です。
Ln: 35, Col: 26. cvc-complex-type.2.4.a: 要素'{"urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":BIC}'で始まる無効なコンテンツが見つかりました。'{"urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":BICFI, "urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":ClrSysMmbId, "urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":LEI, "urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":Nm, "urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":PstlAdr, "urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":Othr}'のいずれかが必要です。
Ln: 47, Col: 30. cvc-complex-type.2.4.a: 要素'{"urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":BIC}'で始まる無効なコンテンツが見つかりました。'{"urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":BICFI, "urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":ClrSysMmbId, "urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":LEI, "urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":Nm, "urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":PstlAdr, "urn:iso:std:iso:20022:tech:xsd:pain.001.001.12":Othr}'のいずれかが必要です。

なお、上述の以下エラーを解消していないとルート要素自体が見つからないので他の検証結果を取得することができないので注意です。

Ln: 2, Col: 66. cvc-elt.1.a: 要素'Document'の宣言が見つかりません。

あとがき

Java のクラスライブラリ javax.xml.validation を使用したスキーマ検証を紹介しました。ビルトインライブラリでスキーマ検証までできるなんて Java はすごいですね。XML は Office 系ファイルの Open XML や有価証券報告書等の XBRL のように様々なところで使われているので使いこなせると便利だと思います。