XML連携はtag名だけを見ても設計できない
XMLはCAD交換、部品表、受発注、装置設定など、長く運用されるデータ連携で使われます。要素を入れ子にでき、属性や名前空間も持てるため表現力がありますが、partやidという文字列だけを見て処理すると、別仕様の要素を同じものとして誤認します。
たとえば、部品を表すp:idと図面を表すd:idはlocal nameが同じidでも意味が異なります。反対に、送信元がpをxへ変更しても、namespace URIが同じなら同じ語彙です。prefixをdatabase列名や分岐条件へ直接保存すると、見た目だけの変更で連携が壊れます。
XML連携では、次の境界を明確にします。
- 名前はnamespace URIとlocal nameの組で識別する
- XSDで要素、属性、順序、型、出現回数を検証する
- 外部entity、DTD、network参照を入力境界で無効化する
- XSD合格後に業務ID、日時、単位、参照関係を検証する
- schema versionと変換規則を記録して再実行可能にする
この記事では、部品データを例に、XSD 1.0とPythonのlxmlを使ってXMLを検証し、APIやDBへ渡せるobjectへ変換します。
namespace URIとlocal nameが本当の名前になる
XML Namespacesでは、expanded nameはnamespace nameとlocal nameの組です。p:partのpはそのURIへ結び付けるための短いprefixにすぎません。
<p:part xmlns:p="urn:ekplabz:parts:1" schemaVersion="1.0">
<p:partId>0012</p:partId>
</p:part>
このroot要素の識別情報は、次の組になります。
| 項目 | 値 | 設計上の役割 |
|---|---|---|
| prefix | p | XML文書内の省略名。変更されてもよい |
| namespace URI | urn:ekplabz:parts:1 | 語彙を識別する安定した名前 |
| local name | part | 語彙内の要素名 |
| expanded name | {urn:ekplabz:parts:1}part | program内で比較する実質的な名前 |
同じ文書をx:partで書いても、xが同じURIへ対応していれば意味は変わりません。したがって、XPathや検索処理では自分でnamespace mapを定義し、受信文書のprefix文字列へ依存しないようにします。
default namespaceにも注意が必要です。xmlns="urn:ekplabz:parts:1"はprefixなしの要素へ適用されますが、prefixなしの属性へは適用されません。要素と属性を同じ感覚で扱わず、XSDのelementFormDefaultとattributeFormDefaultを明示します。
最小XMLと変換後のobject
受け付けるXMLを次の形に固定します。
<?xml version="1.0" encoding="UTF-8"?>
<p:part xmlns:p="urn:ekplabz:parts:1" schemaVersion="1.0">
<p:partId>0012</p:partId>
<p:active>true</p:active>
<p:mass unit="kg">2.750</p:mass>
<p:releasedAt>2026-10-24T09:30:00+09:00</p:releasedAt>
<p:material>
<p:materialId>AL-6061</p:materialId>
<p:name>Aluminum alloy</p:name>
</p:material>
</p:part>
検証・正規化後は次のobjectへ変換します。
{
"schema_version": "1.0",
"part_id": "0012",
"active": true,
"mass": {
"value": "2.750",
"unit": "kg"
},
"released_at": "2026-10-24T09:30:00+09:00",
"material": {
"material_id": "AL-6061",
"name": "Aluminum alloy"
}
}
part_idは計算対象ではないためstringのまま保持します。質量はdecimalをbinary floatへ急いで変換せず、正規化したdecimal文字列と単位に分離します。日時はoffset付きであることを業務検証し、NULL、要素なし、空要素も別状態として扱います。
XSDで構造と型を固定する
次のXSD 1.0は、target namespace、要素順、必須項目、出現回数、ID pattern、数値範囲、単位、schema versionを定義します。
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:p="urn:ekplabz:parts:1"
targetNamespace="urn:ekplabz:parts:1"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xs:element name="part">
<xs:complexType>
<xs:sequence>
<xs:element name="partId">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[A-Z0-9][A-Z0-9-]{0,39}"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="active" type="xs:boolean"/>
<xs:element name="mass">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:decimal">
<xs:attribute name="unit" use="required" fixed="kg"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="releasedAt" type="xs:dateTime"/>
<xs:element name="material">
<xs:complexType>
<xs:sequence>
<xs:element name="materialId" type="xs:string"/>
<xs:element name="name" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="schemaVersion" use="required" fixed="1.0"/>
</xs:complexType>
</xs:element>
</xs:schema>
elementFormDefault="qualified"により、local要素もtarget namespaceへ所属させます。xs:sequenceは順序を固定し、既定のminOccurs="1"とmaxOccurs="1"は欠落や重複を拒否します。未知の要素を受け入れたい場合は、無条件に許可するのではなく、拡張用namespaceと配置位置を決めてxs:anyを設計します。
XSDで検証できることとできないこと
XSDは構造とdatatypeの検証に強い一方、連携処理全体を保証するものではありません。
| 検証対象 | XSD | 別の検証層 |
|---|---|---|
| 必須要素、順序、出現回数 | 対応 | 契約testで補強 |
| string、boolean、decimal、dateTime | 対応 | 利用libraryの対応versionを確認 |
| pattern、範囲、固定値 | 対応 | 業務規則が複雑ならcode化 |
| materialIdの参照先がDBに存在する | 非対応 | lookupまたは外部キー |
| BOMに循環がない | 非対応 | graph検証 |
| requestの再送が二重登録にならない | 非対応 | 冪等keyと一意制約 |
| 利用者が更新権限を持つ | 非対応 | 認証・認可 |
xs:dateTimeはtimezoneなしの字句も表現できるため、offset必須という業務規則は追加検証します。XSD validatorの対応versionも固定します。次のPython例で使うlxml.etree.XMLSchemaはXML Schema 1.0を対象とし、XSD 1.1固有機能を前提にしません。
Pythonで安全にparseしXSD検証する
次の例は入力byte数を制限し、UTF-8以外を拒否します。DTD、外部entityの展開、network参照、巨大tree向け緩和を無効化し、XSD合格後にnamespace対応XPathで値を取得します。
from __future__ import annotations
from datetime import datetime
from decimal import Decimal, InvalidOperation
from pathlib import Path
from lxml import etree
PART_NS = "urn:ekplabz:parts:1"
NS = {"p": PART_NS}
def secure_parser() -> etree.XMLParser:
return etree.XMLParser(
resolve_entities=False,
no_network=True,
load_dtd=False,
huge_tree=False,
recover=False,
)
def load_schema(path: Path) -> etree.XMLSchema:
schema_doc = etree.parse(str(path), parser=secure_parser())
return etree.XMLSchema(schema_doc)
def require_offset_datetime(value: str) -> str:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
if parsed.utcoffset() is None:
raise ValueError("releasedAt must include Z or a UTC offset")
return value
def parse_part_xml(
data: bytes,
schema: etree.XMLSchema,
max_bytes: int = 1_000_000,
) -> dict[str, object]:
if len(data) > max_bytes:
raise ValueError("XML input size limit exceeded")
text = data.decode("utf-8", errors="strict")
if "<!DOCTYPE" in text.upper():
raise ValueError("DOCTYPE is not allowed")
root = etree.fromstring(data, parser=secure_parser())
expected_root = f"{{{PART_NS}}}part"
if root.tag != expected_root:
raise ValueError(f"unexpected root element: {root.tag}")
schema.assertValid(root)
def one_text(path: str) -> str:
values = root.xpath(path, namespaces=NS)
if len(values) != 1 or values[0].text is None:
raise ValueError(f"expected one text node at {path}")
return values[0].text
mass_node = root.xpath("p:mass", namespaces=NS)[0]
try:
mass_value = format(Decimal(mass_node.text), "f")
except (InvalidOperation, TypeError) as exc:
raise ValueError("invalid mass") from exc
active_text = one_text("p:active")
return {
"schema_version": root.get("schemaVersion"),
"part_id": one_text("p:partId"),
"active": active_text in {"true", "1"},
"mass": {
"value": mass_value,
"unit": mass_node.get("unit"),
},
"released_at": require_offset_datetime(one_text("p:releasedAt")),
"material": {
"material_id": one_text("p:material/p:materialId"),
"name": one_text("p:material/p:name"),
},
}
XSD errorはDocumentInvalidとして発生し、validatorのerror logから行番号や検証理由を取得できます。外部へ返すerrorには安定したerror codeと対象pathを含め、内部pathやXML全文は返しません。XMLには個人情報やcredentialが入り得るため、logへ記録する要素をallowlist化します。
正常系と異常系を固定testにする
少なくとも次のcaseを自動testします。
- 正常な最小XMLがXSDを通り、期待するobjectへ変換される
- prefixを
pからxへ変えてもnamespace URIが同じなら成功する - namespace URI、root要素、schema versionの違いを拒否する
- 必須要素の欠落、重複、順序違い、未知要素を拒否する
partIdの先頭ゼロをstringとして保持するactiveのtrue、false、1、0を意図どおり変換する- massの単位違い、非数値、上限超過を拒否する
- timezoneなし日時を業務検証で拒否する
- DTD、外部entity、過大入力、深すぎるtreeを拒否する
- XSDは正しいがmaterialIdの参照先がない入力をDB層で拒否する
文字コードは入口契約でUTF-8へ固定します。XML declarationと実byteのencodingが矛盾する入力を黙って補正しません。空要素<p:name/>、要素欠落、xsi:nil="true"も同じ意味ではないため、NULLを必要とするfieldだけXSDでnillableを明示します。
version変更と冪等性を分けて設計する
namespace URIへversionを含めるか、schemaVersion属性だけで管理するかを最初に決めます。URIを変える方式は語彙の破壊的変更を明確にできますが、XPath、XSD、変換器も新URIへ対応させる必要があります。属性方式は小さな更新に向きますが、同じnamespace内の互換範囲を厳密に管理しなければなりません。
変換器は、対応するnamespace URI、XSD hash、schema version、変換規則versionをmanifestへ残します。同じ入力を再処理するときは、業務IDとsource revisionを一意keyにし、DBの一意制約やtransactionで重複登録を防ぎます。XSD合格と冪等性は別の判定です。
署名やhash比較では、prefix、属性順、空白などの表現差が問題になります。単純なXML文字列のhashではなく、合意したXML canonicalization方式を使います。正規化前の原文hashと、検証後の業務object hashを目的別に分けると監査しやすくなります。
CAD・BOM・API連携へ再利用する
CADやBOMのXMLでは、部品、材質、図面、単位、幾何情報を別namespaceへ分けられます。namespaceは単なる分類ラベルではなく、語彙の所有者と意味の境界です。異なる語彙を一つのXSDへ無理に詰めず、xs:importで明示的に組み合わせます。ただし、外部schemaを実行時にnetwork取得せず、承認済みversionをlocalへ固定してhashを検証します。
JSON APIへ変換するときも、XML要素名を機械的にkeyへ置換するだけでは不十分です。属性と要素、順序、mixed content、namespace、decimal、日時、NULLの規則を変換仕様として残します。DBではID、revision、外部キー、単位制約を列とconstraintへ対応付けます。
公開用XMLと内部用XMLも分離します。API key、内部file path、原価、個人情報を同じdocumentへ混ぜず、権限の異なる保存領域から実行時に関連付けます。
まとめ
XML連携を安定させる中心は、見た目のprefixではなくnamespace URI、local name、XSDを契約として扱うことです。
- 要素はnamespace URIとlocal nameの組で識別する
- prefix変更を意味変更として扱わない
targetNamespaceとelementFormDefaultを明示する- XSDで型、順序、出現回数、固定値を検証する
- 外部entity、DTD、network参照、過大入力を拒否する
- XSD後に参照整合性、timezone、権限、冪等性を検証する
- schema、XSD hash、変換器versionを履歴管理する
- CAD・BOM・APIごとの語彙をnamespaceで分離する
XML instance、XSD、変換code、正常系・異常系testを同じrepositoryで管理すれば、長期間使う設計データでも「どの語彙の、どのversionの、どの型か」を機械的に確認できます。
