「XML名前空間を壊さない設計|QName・XSD・Python検証」の内容を表す技術イラスト
UNIT 00LAB NOTES FIG.00 · EYECATCH

XML名前空間を壊さない設計|QName・XSD・Python検証

XML名前空間を壊さず扱う設計を解説。QName、namespace URI、XSD 1.0、型・順序・出現回数、安全なPython parse、異常系test、version管理、CAD・BOM・API連携まで整理します。

DOC NO.EKP-00-1171
DATE
REVB
READ約 16 分
目次

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要素の識別情報は、次の組になります。

項目値設計上の役割
prefixpXML文書内の省略名。変更されてもよい
namespace URIurn:ekplabz:parts:1語彙を識別する安定した名前
local namepart語彙内の要素名
expanded name{urn:ekplabz:parts:1}partprogram内で比較する実質的な名前

同じ文書を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の、どの型か」を機械的に確認できます。

参考情報

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次