概要

ZEDI(全銀EDIシステム)は、2018年に稼働を開始した XML ベースの金融EDI基盤です。従来の全銀フォーマットでは振込データに付帯できる情報が限られていましたが、ZEDI では ISO 20022 に準拠した XML メッセージフォーマットを採用し、請求書番号・支払通知番号・明細情報といった商取引に関わる付帯情報を振込データとあわせて送受信できるようになりました。企業間の売掛金消込や入金照合の自動化を推進するための仕組みであり、全国銀行協会が技術仕様書を公開しています。この記事では、ZEDI の XML 電文を Java 標準 API(DOM / StAX)で生成・パースする方法を扱います。名前空間の扱い、XXE 対策、文字コードの指定、スキーマ検証の要否といった実装上の注意点を整理し、振込付帯情報の読み書きを業務で使える形にまとめます。ISO 20022 の pain.001(振込指図)メッセージ構造に沿って、RmtInf(付帯情報)ブロックの組み立てとパースに焦点を当てます。

使いどころ

振込データに請求書番号を付帯し、受取側で売掛金の自動消込に利用する

取引先から受信した ZEDI 電文をパースし、支払通知番号をもとに入金と請求の自動照合を行う

月次の一括振込バッチで、複数の振込取引にそれぞれ異なる付帯情報(明細番号・契約番号)を付与して送信する

経理システムから出力した振込データを ZEDI XML 形式に変換し、インターネットバンキング経由で銀行に送信する

受信した ZEDI 電文の付帯情報を DB に取り込み、未消込の売掛金一覧と突合するバッチ処理を実装する

コード例

ZEDI XML 電文の生成とパース(DOM API)
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;

/**
 * ZEDI(全銀EDIシステム)XML 電文の生成・パース。
 * ISO 20022 の pain.001(顧客振込指図)メッセージ構造に基づき、
 * 振込付帯情報(RmtInf)の読み書きを行う。
 *
 * DOM API を使用する理由:
 * - 要素の追加・属性設定が直感的で、電文の組み立てに適している
 * - 名前空間付き要素の生成が createElementNS() で簡潔に書ける
 * - 1電文ずつの処理であればメモリ消費も問題にならない
 */
public class ZenginEdiSample {

    // ISO 20022 pain.001 の名前空間 URI
    // ZEDI 技術仕様書で定められた名前空間を正確に使用すること
    private static final String ZEDI_NS =
        "urn:iso:std:iso:20022:tech:xsd:pain.001.001.03";

    /**
     * パース結果を保持する record。
     * 不変オブジェクトとして振込付帯情報を安全に受け渡せる。
     */
    record ZediRemittanceInfo(
        String invoiceNumber,    // 請求書番号(RfrdDocInf/Nb に対応)
        long amount,             // 振込金額(InstdAmt のテキスト値)
        String currency,         // 通貨コード(InstdAmt の Ccy 属性)
        String payeeName,        // 受取人名(Cdtr/Nm に対応)
        String additionalInfo    // 追加付帯情報(AddtlRmtInf に対応)
    ) {}

    /**
     * ZEDI XML 電文を DOM API で生成する。
     *
     * ISO 20022 の構造:
     * Document
     *   └ CstmrCdtTrfInitn(顧客振込指図)
     *       ├ GrpHdr(グループヘッダー: メッセージID等)
     *       └ PmtInf(支払情報)
     *           ├ Dbtr(支払人情報)
     *           └ CdtTrfTxInf(個別振込取引)
     *               ├ Amt/InstdAmt(振込金額 + 通貨コード)
     *               ├ Cdtr(受取人情報)
     *               └ RmtInf/Strd(構造化付帯情報)
     *                   ├ RfrdDocInf/Nb(請求書番号)
     *                   └ AddtlRmtInf(追加付帯情報)
     */
    public static String buildZediMessage(
            String invoiceNumber,
            long paymentAmount,
            String payerName,
            String payeeName,
            String paymentNoticeId) throws Exception {

        // DOM ファクトリの設定
        // setNamespaceAware(true) は ZEDI XML の生成・パースで必須
        var factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);
        var doc = factory.newDocumentBuilder().newDocument();

        // ルート要素: Document(ISO 20022 メッセージのエンベロープ)
        // createElementNS で名前空間付き要素を作成する
        var root = doc.createElementNS(ZEDI_NS, "Document");
        doc.appendChild(root);

        // CstmrCdtTrfInitn: Customer Credit Transfer Initiation
        // 顧客からの振込指図メッセージ全体を包含する要素
        var initiation = doc.createElementNS(ZEDI_NS, "CstmrCdtTrfInitn");
        root.appendChild(initiation);

        // GrpHdr: Group Header — 電文の識別情報
        var grpHdr = doc.createElementNS(ZEDI_NS, "GrpHdr");
        initiation.appendChild(grpHdr);
        // MsgId: メッセージを一意に識別するID
        // 実務ではシステム固有の採番ルールに従う
        appendTextElement(doc, grpHdr, "MsgId",
            "MSG-" + System.currentTimeMillis());

        // PmtInf: Payment Information — 支払情報のブロック
        var pmtInf = doc.createElementNS(ZEDI_NS, "PmtInf");
        initiation.appendChild(pmtInf);

        // Dbtr: Debtor(支払人)
        var dbtr = doc.createElementNS(ZEDI_NS, "Dbtr");
        pmtInf.appendChild(dbtr);
        // Nm: 支払人の名称
        appendTextElement(doc, dbtr, "Nm", payerName);

        // CdtTrfTxInf: Credit Transfer Transaction Information
        // 個別の振込取引に対応する要素(1電文に複数可)
        var txInf = doc.createElementNS(ZEDI_NS, "CdtTrfTxInf");
        pmtInf.appendChild(txInf);

        // Amt: Amount ブロック
        var amt = doc.createElementNS(ZEDI_NS, "Amt");
        txInf.appendChild(amt);
        // InstdAmt: Instructed Amount(指定金額)
        // Ccy 属性で通貨コードを指定する(日本円 = JPY)
        var instdAmt = doc.createElementNS(ZEDI_NS, "InstdAmt");
        instdAmt.setAttribute("Ccy", "JPY");
        instdAmt.setTextContent(String.valueOf(paymentAmount));
        amt.appendChild(instdAmt);

        // Cdtr: Creditor(受取人)
        var cdtr = doc.createElementNS(ZEDI_NS, "Cdtr");
        txInf.appendChild(cdtr);
        appendTextElement(doc, cdtr, "Nm", payeeName);

        // RmtInf: Remittance Information — 付帯情報
        // ZEDI の核心部分。請求書番号や支払通知番号をここに格納する
        var rmtInf = doc.createElementNS(ZEDI_NS, "RmtInf");
        txInf.appendChild(rmtInf);

        // Strd: Structured — 構造化された付帯情報
        var strd = doc.createElementNS(ZEDI_NS, "Strd");
        rmtInf.appendChild(strd);

        // RfrdDocInf: Referred Document Information — 参照ドキュメント
        // 請求書番号などの文書識別情報を格納する
        var rfrdDocInf = doc.createElementNS(ZEDI_NS, "RfrdDocInf");
        strd.appendChild(rfrdDocInf);
        // Nb: Number — 請求書番号
        appendTextElement(doc, rfrdDocInf, "Nb", invoiceNumber);

        // AddtlRmtInf: Additional Remittance Information
        // 支払通知番号など、構造化しきれない追加情報を格納する
        appendTextElement(doc, strd, "AddtlRmtInf",
            "PaymentNotice:" + paymentNoticeId);

        // XML 文字列への変換
        // Transformer で DOM ツリーを文字列にシリアライズする
        var transformer = TransformerFactory.newInstance().newTransformer();
        // インデント付き出力(デバッグ・ログ用途で可読性を確保)
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        // エンコーディングを UTF-8 に明示(ZEDI の標準エンコーディング)
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");

        var writer = new StringWriter();
        transformer.transform(new DOMSource(doc), new StreamResult(writer));
        return writer.toString();
    }

    /**
     * 名前空間付きのテキスト要素を生成し、親要素に追加するヘルパー。
     * ZEDI 電文では全要素が同一名前空間に属するため、
     * createElementNS を共通化して記述量を減らす。
     */
    private static void appendTextElement(
            Document doc, Element parent, String localName, String text) {
        var elem = doc.createElementNS(ZEDI_NS, localName);
        elem.setTextContent(text);
        parent.appendChild(elem);
    }

    /**
     * ZEDI XML 電文をパースし、付帯情報を record のリストとして返す。
     *
     * パースでも DOM を使用する。1電文ずつの処理であればメモリ消費は問題にならず、
     * getElementsByTagNameNS() で名前空間付き要素を直接取得できるため実装が簡潔。
     * 大量電文の一括処理には StAX(XMLStreamReader)への切り替えを検討すること。
     */
    public static List<ZediRemittanceInfo> parseZediMessage(String xml)
            throws Exception {

        var factory = DocumentBuilderFactory.newInstance();
        // 名前空間対応を有効化(ZEDI では必須)
        factory.setNamespaceAware(true);
        // XXE 対策: 外部 DTD の読み込みを禁止
        factory.setFeature(
            "http://apache.org/xml/features/disallow-doctype-decl", true);
        // XXE 対策: 外部エンティティ参照を無効化
        factory.setFeature(
            "http://xml.org/sax/features/external-general-entities", false);

        var doc = factory.newDocumentBuilder()
                .parse(new InputSource(new StringReader(xml)));
        doc.getDocumentElement().normalize();

        var results = new ArrayList<ZediRemittanceInfo>();

        // CdtTrfTxInf(個別振込取引)を名前空間付きで全件取得
        var txNodes = doc.getElementsByTagNameNS(ZEDI_NS, "CdtTrfTxInf");

        for (int i = 0; i < txNodes.getLength(); i++) {
            var txElem = (Element) txNodes.item(i);

            // 振込金額: InstdAmt 要素のテキストと Ccy 属性
            var amountText = getTextNS(txElem, "InstdAmt");
            var currency = getAttributeNS(txElem, "InstdAmt", "Ccy");

            // 受取人名: Cdtr/Nm(ネストした要素の取得)
            var payeeName = getNestedTextNS(txElem, "Cdtr", "Nm");

            // 請求書番号: RfrdDocInf/Nb
            var invoiceNumber = getTextNS(txElem, "Nb");

            // 追加付帯情報: AddtlRmtInf
            var additionalInfo = getTextNS(txElem, "AddtlRmtInf");

            // record に格納して返す
            results.add(new ZediRemittanceInfo(
                invoiceNumber,
                amountText.isEmpty() ? 0L : Long.parseLong(amountText),
                currency,
                payeeName,
                additionalInfo
            ));
        }
        return results;
    }

    /** 名前空間付き要素のテキストを取得する(見つからなければ空文字) */
    private static String getTextNS(Element parent, String localName) {
        var nodes = parent.getElementsByTagNameNS(ZEDI_NS, localName);
        return nodes.getLength() > 0
            ? nodes.item(0).getTextContent() : "";
    }

    /** 名前空間付き要素の属性値を取得する */
    private static String getAttributeNS(
            Element parent, String localName, String attrName) {
        var nodes = parent.getElementsByTagNameNS(ZEDI_NS, localName);
        return nodes.getLength() > 0
            ? ((Element) nodes.item(0)).getAttribute(attrName) : "";
    }

    /** ネストされた名前空間付き要素のテキストを取得する */
    private static String getNestedTextNS(
            Element parent, String outerName, String innerName) {
        var outerNodes = parent.getElementsByTagNameNS(ZEDI_NS, outerName);
        if (outerNodes.getLength() > 0) {
            var outerElem = (Element) outerNodes.item(0);
            var innerNodes =
                outerElem.getElementsByTagNameNS(ZEDI_NS, innerName);
            return innerNodes.getLength() > 0
                ? innerNodes.item(0).getTextContent() : "";
        }
        return "";
    }

    public static void main(String[] args) throws Exception {
        // ZEDI 電文を生成する
        var xml = buildZediMessage(
            "INV-2025-001234",      // 請求書番号
            1500000L,               // 支払金額(150万円)
            "株式会社サンプル商事",    // 支払人名
            "株式会社テスト工業",      // 受取人名
            "PN-2025-00567"         // 支払通知番号
        );
        System.out.println("=== 生成した ZEDI XML 電文 ===");
        System.out.println(xml);

        // 生成した XML をパースして付帯情報を取り出す
        var remittanceList = parseZediMessage(xml);
        System.out.println("=== パース結果 ===");
        for (var info : remittanceList) {
            System.out.println("請求書番号: " + info.invoiceNumber());
            System.out.println("支払金額: " + info.amount() + " "
                + info.currency());
            System.out.println("受取人: " + info.payeeName());
            System.out.println("追加情報: " + info.additionalInfo());
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var で DOM 操作のローカル変数を簡潔に記述でき、record でパース結果を不変かつ型安全に保持できる。テキストブロックでサンプル XML の埋め込みも可読性が高い。

Java 17
// Java 17: var + record でパース結果を型安全に保持
record ZediRemittanceInfo(String invoiceNumber, long amount,
    String currency, String payeeName, String additionalInfo) {}

var factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
var doc = factory.newDocumentBuilder()
    .parse(new InputSource(new StringReader(xml)));

// record コンストラクタで不変オブジェクトに格納
results.add(new ZediRemittanceInfo(invoiceNumber,
    Long.parseLong(amount), currency, payeeName, additionalInfo));

Library Comparison

Java 標準 API(DOM)電文1件ずつの生成やパースで、XML全体をツリーとして操作したい場合。要素の追加・削除・属性設定が自在にでき、ZEDI 電文の組み立てに向いている。XML 全体をメモリに展開するため、数千件の電文を一括処理する場合はメモリ消費に注意が必要。数十MB を超える XML には不向き。
Java 標準 API(StAX)大量の ZEDI 電文を順次パースするバッチ処理。カーソル方式で1要素ずつ読み進めるため、メモリ効率がよい。前方向にしか進めないため、パース結果を組み立てるロジックが DOM より複雑になる。XML の生成には XMLStreamWriter を使うが、名前空間の管理が手間になる。
JAXB(jakarta.xml.bind)ISO 20022 の XSD スキーマから Java クラスを自動生成し、オブジェクトと XML の双方向マッピングを行いたい場合。大規模な ZEDI 対応システムに向く。Java 11 以降は JDK に含まれず外部依存が必要。XSD からのコード生成の初期構築コストが高く、小規模な連携には過剰。

注意点

ZEDI の XML は名前空間付きである。DocumentBuilderFactory.setNamespaceAware(true) を設定しないと getElementsByTagNameNS() で要素を取得できない。名前空間なしの getElementsByTagName() では正しくヒットしない場合がある。

外部から受信した XML をパースする際は、XXE(XML External Entity)攻撃を防ぐために DTD 処理と外部エンティティの読み込みを無効化すること。DocumentBuilderFactory では disallow-doctype-decl と external-general-entities の両方を設定する。

Transformer でXML文字列を出力する際、OutputKeys.ENCODING を指定しないとプラットフォーム依存のエンコーディングが使われる可能性がある。ZEDI 電文は UTF-8 が標準であるため、明示的に UTF-8 を設定すること。

ISO 20022 のメッセージには多数の任意要素がある。パース時に getElementsByTagNameNS() の結果が空(getLength() == 0)になるケースを必ずハンドリングし、NullPointerException を防ぐこと。

ZEDI の技術仕様書で定められた要素名(CstmrCdtTrfInitn、RmtInf、RfrdDocInf など)は ISO 20022 の略語に基づいている。仕様書のフィールド定義と XML 要素名の対応を事前に確認してから実装に入ること。

FAQ

ZEDI と従来の全銀フォーマットの違いは何ですか。

全銀フォーマットは固定長テキスト形式で付帯情報の自由度が低いのに対し、ZEDI は ISO 20022 準拠の XML 形式を採用しており、請求書番号・支払通知番号・明細情報などの商取引データを構造化して送受信できます。

ZEDI の XML スキーマ(XSD)はどこで入手できますか。

全国銀行協会の ZEDI 関連資料ページおよび全銀EDIシステム技術仕様書に、ISO 20022 ベースのメッセージ定義が公開されています。pain.001(振込指図)や camt.054(入出金明細)などのスキーマを参照してください。

DOM と StAX のどちらで ZEDI 電文を処理すべきですか。

電文の生成や要素の自在な操作が必要なら DOM、大量電文の順次パースや省メモリ処理が求められるバッチなら StAX が適しています。生成は DOM、パースは StAX という使い分けも実務では一般的です。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます