XMLパースは「外部仕様を自分のオブジェクトに落とし込む」技
XML生成が「こちらの世界のデータを外に出す」なら、
XMLパースはその逆で、「外の世界から来た XML を、自分の世界(Java のオブジェクト)に安全に落とし込む」技です。
ここを雑にやると、「タグがなかった」「想定外の値が入っていた」「文字コードでコケた」みたいな、本番でしか出ないイヤなバグが出ます。
だからこそ、XMLパース用のユーティリティを一つ決めておくと、コード全体がかなり落ち着きます。
基本の考え方:文字列を直接いじらず「パーサに任せる」
substring や indexOf で XML を触るのはやめる
まず大前提として、次のようなコードは封印してほしいです。
// 絶対にやめたほうがいい例
int start = xml.indexOf("<name>");
int end = xml.indexOf("</name>");
String name = xml.substring(start + 6, end);
Java一見動きそうですが、改行や空白、名前空間、属性が入った瞬間に壊れます。
XML は「構造化データ」なので、文字列操作ではなく「XML パーサ」に任せるのが鉄則です。
Java には標準で DOM(ツリー構造)や SAX(イベント駆動)のパーサが用意されていますが、
ここではまず「DOM を使って XML をオブジェクトとして扱う」流れから見ていきます。
DOM を使った「XML → DOMツリー」の基本
DocumentBuilder で XML を読み込む
XML を DOM として読み込む最小コードはこんな感じです。
import org.w3c.dom.Document;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
public final class XmlParser {
private XmlParser() {}
public static Document parse(String xml) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 必要に応じて安全設定を入れる(後述)
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(new java.io.ByteArrayInputStream(xml.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
} catch (Exception e) {
throw new IllegalArgumentException("XML パースに失敗しました。入力が正しい XML か確認してください。", e);
}
}
}
Java使い方はこうです。
String xml = """
<user>
<id>u-001</id>
<name>山田太郎</name>
</user>
""";
Document doc = XmlParser.parse(xml);
Javaここで深掘りしたい重要ポイントは、「XML を一度 Document(DOMツリー)にしてしまえば、タグやテキストを“オブジェクトとして”扱えるようになる」ことです。
文字列を直接いじるのではなく、「ノードをたどる」感覚に切り替わります。
例題:単純な XML から値を取り出す
<user> の <id> と <name> を読む
さきほどの XML から、id と name を取り出してみます。
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
public final class UserXmlReader {
private UserXmlReader() {}
public static String getId(Document doc) {
Element root = doc.getDocumentElement(); // <user>
NodeList idNodes = root.getElementsByTagName("id");
if (idNodes.getLength() == 0) {
return null;
}
return idNodes.item(0).getTextContent();
}
public static String getName(Document doc) {
Element root = doc.getDocumentElement(); // <user>
NodeList nameNodes = root.getElementsByTagName("name");
if (nameNodes.getLength() == 0) {
return null;
}
return nameNodes.item(0).getTextContent();
}
}
Java使い方はこうです。
Document doc = XmlParser.parse(xml);
String id = UserXmlReader.getId(doc);
String name = UserXmlReader.getName(doc);
System.out.println(id); // u-001
System.out.println(name); // 山田太郎
Javaここでのポイントは、「タグ名でノードを検索し、getTextContent で中身を取る」という基本パターンを押さえることです。getTextContent は、子要素を含めたテキストをまとめて返してくれるので、
単純な構造ならこれだけで十分なことが多いです。
例題:XML → POJO へのマッピング
業務エンティティに落とし込む
XML を読む最終目的は、「業務で使えるオブジェクトに落とし込む」ことです。
例えば、さきほどの <user> を User クラスにマッピングしてみます。
public class User {
private final String id;
private final String name;
public User(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() { return id; }
public String getName() { return name; }
}
Javaこれを XML から作るユーティリティを用意します。
import org.w3c.dom.Document;
import org.w3c.dom.Element;
public final class UserXmlMapper {
private UserXmlMapper() {}
public static User fromDocument(Document doc) {
Element root = doc.getDocumentElement();
if (!"user".equals(root.getTagName())) {
throw new IllegalArgumentException("ルート要素が <user> ではありません。実際: " + root.getTagName());
}
String id = getRequiredChildText(root, "id");
String name = getRequiredChildText(root, "name");
return new User(id, name);
}
private static String getRequiredChildText(Element parent, String tagName) {
var nodes = parent.getElementsByTagName(tagName);
if (nodes.getLength() == 0) {
throw new IllegalArgumentException("必須要素 <" + tagName + "> が存在しません。");
}
String text = nodes.item(0).getTextContent();
if (text == null || text.isBlank()) {
throw new IllegalArgumentException("必須要素 <" + tagName + "> が空です。");
}
return text.trim();
}
}
Java使い方はこうです。
Document doc = XmlParser.parse(xml);
User user = UserXmlMapper.fromDocument(doc);
System.out.println(user.getId());
System.out.println(user.getName());
Javaここで深掘りしたい重要ポイントは、「“XML が仕様どおりかどうか”を、マッピングの中でしっかりチェックしている」ことです。getRequiredChildText で「要素がない」「空文字」の場合に例外を投げることで、
「おかしな XML をそのまま業務ロジックに流さない」ようにしています。
XMLパースは「ただ値を取る」のではなく、「仕様に合っているかを確認しながらオブジェクトにする」作業だ、という感覚を持っておくと強いです。
例題:複数要素(リスト)を読む
<users> の中の <user> を全部読む
少しだけ複雑な例として、こんな XML を考えます。
<users>
<user>
<id>u-001</id>
<name>山田太郎</name>
</user>
<user>
<id>u-002</id>
<name>佐藤花子</name>
</user>
</users>
これを List<User> にしたいとします。
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import java.util.ArrayList;
import java.util.List;
public final class UsersXmlMapper {
private UsersXmlMapper() {}
public static List<User> fromDocument(Document doc) {
Element root = doc.getDocumentElement();
if (!"users".equals(root.getTagName())) {
throw new IllegalArgumentException("ルート要素が <users> ではありません。");
}
NodeList userNodes = root.getElementsByTagName("user");
List<User> result = new ArrayList<>();
for (int i = 0; i < userNodes.getLength(); i++) {
Element userElem = (Element) userNodes.item(i);
String id = getRequiredChildText(userElem, "id");
String name = getRequiredChildText(userElem, "name");
result.add(new User(id, name));
}
return result;
}
private static String getRequiredChildText(Element parent, String tagName) {
var nodes = parent.getElementsByTagName(tagName);
if (nodes.getLength() == 0) {
throw new IllegalArgumentException("必須要素 <" + tagName + "> が存在しません。");
}
return nodes.item(0).getTextContent().trim();
}
}
Java使い方はこうです。
Document doc = XmlParser.parse(xml);
List<User> users = UsersXmlMapper.fromDocument(doc);
System.out.println(users.size()); // 2
System.out.println(users.get(0).getName()); // 山田太郎
Javaここでのポイントは、「NodeList をループして、1件ずつ POJO に落とし込む」というパターンを覚えることです。
「1要素 → 1オブジェクト」「複数要素 → List<オブジェクト>」という対応が見えてくると、
XML を「ただのテキスト」ではなく「木構造のデータ」として扱えるようになります。
XMLパースで絶対に意識したい「安全性」の話
XXE(外部エンティティ攻撃)対策を入れる
業務で XML を外部から受け取るとき、必ず意識したいのが XXE(XML External Entity)攻撃です。
ざっくり言うと、「XML の中からローカルファイルを読ませたり、外部にアクセスさせたりする」攻撃です。
これを防ぐために、DocumentBuilderFactory に安全設定を入れておきます。
private static DocumentBuilderFactory secureFactory() throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
return factory;
}
Javaそして XmlParser.parse でこれを使います。
public static Document parse(String xml) {
try {
DocumentBuilderFactory factory = secureFactory();
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(new java.io.ByteArrayInputStream(xml.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
} catch (Exception e) {
throw new IllegalArgumentException("XML パースに失敗しました。", e);
}
}
Javaここで深掘りしたいのは、「XMLパースは“ただの構文解析”ではなく、“外部からの入力を扱うセキュリティ境界”でもある」という意識です。
安全設定をユーティリティに閉じ込めておけば、
どこで XML をパースしても、同じ防御が効くようになります。
XMLパースと業務バリデーションを分けて考える
「構造として正しい」と「意味として正しい」は別物
XMLパースが保証してくれるのは、「構文として正しい XML か」「指定したタグが存在するか」までです。
「この金額は 0 以上か」「この状態遷移は許されるか」といった業務ルールは、
パース後に別の層でチェックする必要があります。
つまり、
XMLパース層では
・XML として壊れていないか
・必須タグがあるか
・型(数値/日付など)がパースできるか
業務バリデーション層では
・値が業務ルールに合っているか
・組み合わせとして矛盾していないか
というふうに責務を分けておくと、コードの見通しがかなり良くなります。
まとめ:XMLパースユーティリティで身につけたい感覚
XMLパースは、「文字列を手で切り刻む」のではなく、
「パーサで DOM にしてから、仕様に従ってオブジェクトに落とし込む」ための技です。
押さえておきたい感覚は、まず「DocumentBuilderFactory+DocumentBuilder で XML を DOM にする」こと。
次に、「Element/NodeList を使ってタグをたどり、必須要素の有無や中身をチェックしながら POJO にマッピングする」こと。
そして、「安全設定(XXE 対策)をユーティリティに閉じ込めて、どこでパースしても同じ防御が効くようにする」ことです。
