0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SOAP XMLの名前空間はなぜ分かりにくいのか:prefix・default namespace・QNameを最小例で理解する

0
Posted at

はじめに

SOAP XMLをWireMockで返したとき、XMLエディタではエラーがないのに、JAXBのフィールドがnullになったり、unmarshal時に期待する要素として認識されなかったりすることがあります。ルート要素の名前空間が一致しない場合は、UnmarshalExceptionになることもあります。

このとき原因になりやすいのが名前空間です。ns2のような接頭辞が付いているかではなく、要素がどの名前空間URIに属し、どのローカル名を持つかが重要です。

この記事では、prefix、default namespace、QNameを最小例で整理し、WireMockのSOAP fixtureを作るときにどこを確認すればよいかを説明します。

先に結論

XML要素を識別するとき、重要なのは接頭辞の文字列ではなく、QNameを名前空間宣言に照らして解決した結果です。

XML上では、svc:outPayloadのような表記をQNameと呼びます。QNameを解決すると、要素は名前空間URIとローカル名の組で識別できます。W3Cではこの組をexpanded nameと呼びます。本記事では分かりやすさのため、次の形で表します。

{名前空間URI}ローカル名

例えば、次の2つは接頭辞が違っても同じexpanded nameになります。

<ns1:outPayload xmlns:ns1="http://example.com/service" />
<service:outPayload xmlns:service="http://example.com/service" />

どちらも{http://example.com/service}outPayloadです。一方、xmlns:svcを宣言しただけの<outPayload>は名前空間なしの{}outPayloadです。JAXBが前者を期待していれば一致しません。

prefixは名前空間URIの別名である

XML Namespacesでは、名前空間はURI参照で識別されます。prefixは長いURIを毎回書かないための別名です。

<svc:outPayload xmlns:svc="http://example.com/service">
  <svc:resultId>result-001</svc:resultId>
</svc:outPayload>

svcをns2やxに変えても、同じURIへ結び付いていれば、要素のexpanded nameは同じです。XMLの差分を見るときは、prefixの文字列だけでなくURIとローカル名を確認します。

宣言しただけでは、要素は名前空間に入らない

次のXMLでは、xmlns:svcはsvcをURIへ結び付ける宣言にすぎません。

<outPayload xmlns:svc="http://example.com/service">
  <resultId>result-001</resultId>
</outPayload>

outPayloadとresultIdにはsvc:がなく、default namespaceもないため、どちらも名前空間なしです。

{}outPayload
{}resultId

名前空間付きにするにはprefixを付けます。

<svc:outPayload xmlns:svc="http://example.com/service">
  <svc:resultId>result-001</svc:resultId>
</svc:outPayload>

この違いはXMLが整形式かどうかでは検出できません。どちらも解析できますが、JAXBが期待する要素は異なります。

default namespaceを使うと、接頭辞なし要素にもURIが付く

接頭辞を毎回書きたくない場合はdefault namespaceを使えます。

<outPayload xmlns="http://example.com/service">
  <resultId>result-001</resultId>
</outPayload>

この場合、スコープ内の接頭辞なし要素はdefault namespaceに属し、両要素は{http://example.com/service}になります。xmlns:svc="..."とxmlns="..."を取り違えないことが重要です。

default namespaceは接頭辞なし属性には適用されない

default namespaceは接頭辞なし属性には適用されません。

<outPayload xmlns="http://example.com/service" status="ok">
  <resultId>result-001</resultId>
</outPayload>

このXMLではoutPayloadとresultIdはサービスの名前空間に属しますが、statusは名前空間なしです。属性も名前空間に属させるなら、svc:statusのようにprefixを付けます。

SOAPでは複数の名前空間が同時に出る

SOAP XMLではEnvelope用、サービス用、共通型用など複数の名前空間が同時に出ます。

<soapenv:Envelope
  xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
  xmlns:svc="http://example.com/report/service"
  xmlns:com="http://example.com/common">
  <soapenv:Body>
    <svc:getReportResponse>
      <svc:outPayload>
        <com:resultId>result-001</com:resultId>
      </svc:outPayload>
    </svc:getReportResponse>
  </soapenv:Body>
</soapenv:Envelope>

resultIdというローカル名が同じでもURIが違えば別要素です。要素名だけで比較せず、名前空間URIとローカル名を確認します。

JAXBが見ているのは見た目ではなくXMLの契約

Jakarta XML Bindingは、XMLとJavaオブジェクトを対応付けます。@XmlRootElementや@XmlElementのnameとnamespaceは、要素マッピングを決めます。

@XmlRootElement(name = "outPayload", namespace = "http://example.com/service")
public class OutPayload {
    @XmlElement(name = "resultId", namespace = "http://example.com/service")
    private String resultId;
}

このクラスが期待しているのは、名前空間付きのoutPayloadとresultIdです。fixtureを作るときは、見た目を手で整える前に、WSDL、XSD、生成クラスのアノテーション、ObjectFactoryを確認します。

XPathを書くときも名前空間を意識する

WireMockのmatchesXPathでSOAP bodyを条件にするときも、名前空間は影響します。ローカル名だけを見る//lineNumberは、意図せず別の名前空間の要素を拾うことがあります。

{
  "matchesXPath": "//svc:lineNumber[text()='target-line']",
  "xPathNamespaces": {
    "svc": "http://example.com/report/service"
  }
}

XPath側のprefixはWireMock設定内で使う別名であり、実際のSOAP XMLのprefix文字列と一致する必要はありません。同じURIを指定することが重要です。WireMockのXPathマッチングでは、xPathNamespacesをmatchesXPathと同じ階層へ置く形式が案内されています。

WireMock 3.12.0以降では、equalToXmlのnamespaceAwarenessで名前空間比較の扱いも設定できます。名前空間そのものを契約として固定したい場合は、STRICTを検討します。

名前空間の不一致を調べる順番

JAXBでフィールドの値が取れない、またはunmarshal時に期待する要素として認識されないときは、次の順で確認します。

  1. WSDLまたはXSDからルート要素のURIとローカル名を確認する
  2. JAXB生成クラスの@XmlRootElementと@XmlElementを確認する
  3. 実際のXMLの要素が同じexpanded nameになっているか確認する
  4. default namespaceとprefix宣言を取り違えていないか確認する
  5. 接頭辞なし属性の名前空間を確認する
  6. 正常に動くXMLとfixtureの差分を比較する
  7. 実際のクライアントでDTOが作られることをテストする

最小の変換テストを置く

名前空間の問題は、XMLの構文チェックだけでは防げません。実際にJAXBを通すテストを置きます。

@Test
void 正しい名前空間のXMLをDTOへ変換する() throws Exception {
    JAXBContext context = JAXBContext.newInstance(OutPayload.class);
    Unmarshaller unmarshaller = context.createUnmarshaller();

    OutPayload payload = (OutPayload) unmarshaller.unmarshal(new StringReader("""
        <svc:outPayload xmlns:svc="http://example.com/service">
          <svc:resultId>result-001</svc:resultId>
        </svc:outPayload>
        """));

    assertThat(payload.getResultId()).isEqualTo("result-001");
}

単体の変換テストでは要素の名前空間URIとローカル名を固定し、結合テストではEnvelope、HTTP header、WireMock mappingを含む契約を確認します。

まとめ

  • QNameはXML上の表記で、解決すると名前空間URIとローカル名の組になります
  • xmlns:svcはprefixを宣言するだけで、接頭辞なし要素をその名前空間へ入れません
  • default namespaceは接頭辞なし要素に適用されますが、接頭辞なし属性には適用されません
  • JAXBで値が取れないときは、整形式ではなくWSDLや生成クラスが期待する名前空間URIとローカル名を確認します
  • WireMockのfixtureも、見た目ではなく外部連携の契約としてテストします
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?