xsi:nillable

XML において値の欠如を表現するのに要素を省くか空要素を使うかの方法があります.

未配送の物品,不明な情報,不適切な情報などを要素の欠落ではなく明示的に表現することが望ましい場合があります.

空要素 <foo></foo> を使うことの問題は,空の値が指定されている xs:positiveInteger にマッチしなくなってしまうことです.確かに,正の整数と空文字の両方を許容するために,xs:positiveInteger と空文字のみを含んだ xs:enumeration(列挙型)の xs:union(ユニオン型) を形成することも可能ですが,厳密には空文字と純粋な無の値は別物です.コードでいうと,null"" の違い,特に Scala での NoneSome("") の違いがあります.

XML Schema ではこの問題を解決するために xsi:nil という特別な属性を使います.

<price xsi:nil="true" />

と書くことで,XML Schema対応のパーサはこれが特別な空要素でその値が空文字ではなくnil値であると解釈します.ある要素において,可空性(nillability, 要素が空の値を取りうるか)と数性(cardinality, 要素が何個現れるか)は別問題であることに注意してください.つまり,minOccurs=0maxOccurs=1 のオプショナルでかつ nillable な要素を定義することもできるし,また,maxOccurs="unbounded" で nillable な要素を定義することもできます.そのような要素を使ってドキュメントの一例を示します:

<foo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <price xsi:nil="true" />
  <price>100</price>
  <price xsi:nil="true" />
</foo>

生成されたScalaコードにおいてnullと非nullのチェックを必要以上に導入したくなかったため,<price xsi:nil="true" />None 扱いされ,これは minOccurs=0 要素において要素が欠落している場合と同じ値になります.

<xs:element name="NillableTest">
  <xs:complexType>
    <xs:sequence>
      <xs:element name="price" type="xs:positiveInteger" nillable="true"/>
      <xs:element name="tax" type="xs:positiveInteger" minOccurs="0" nillable="true"/>
      <xs:element name="tag" type="xs:positiveInteger" minOccurs="0" maxOccurs="unbounded" nillable="true"/>

      <xs:element name="shipTo" type="ipo:Address" nillable="true"/>
      <xs:element name="billTo" type="ipo:Address" minOccurs="0" nillable="true" />
      <xs:element name="via" type="ipo:Address" minOccurs="0" maxOccurs="unbounded" nillable="true" />
    </xs:sequence>
  </xs:complexType>
</xs:element>

以上の定義の要素は以下のコードを生成します:

case class NillableTest(price: Option[Int],
  tax: Option[Int],
  tag: Seq[Option[Int]],
  shipTo: Option[Addressable],
  billTo: Option[Addressable],
  via: Seq[Option[Addressable]]) {
 
  def toXML(namespace: String, elementLabel: String, scope: scala.xml.NamespaceBinding): scala.xml.Node = {
    val prefix = scope.getPrefix(namespace)
    var attribute: scala.xml.MetaData  = scala.xml.Null
 
    scala.xml.Elem(prefix, elementLabel,
      attribute, scope,
      Seq(price match {
          case Some(x) => Seq(scala.xml.Elem(prefix, "price", scala.xml.Null, scope, scala.xml.Text(x.toString)))
          case None => Seq(rt.Helper.nilElem("http://www.example.com/IPO", "price", scope))
        },
        tax match {
          case Some(x) => Seq(scala.xml.Elem(prefix, "tax", scala.xml.Null, scope, scala.xml.Text(x.toString)))
          case None => Seq(rt.Helper.nilElem("http://www.example.com/IPO", "tax", scope))
        },
        tag.map(x => x match {
          case Some(x) => scala.xml.Elem(prefix, "tag", scala.xml.Null, scope, scala.xml.Text(x.toString))
          case None =>   rt.Helper.nilElem("http://www.example.com/IPO", "tag", scope)
        } ),
        shipTo match {
          case Some(x) => x.toXML("http://www.example.com/IPO","shipTo", scope)
          case None => Seq(rt.Helper.nilElem("http://www.example.com/IPO", "shipTo", scope))
        },
        billTo match {
          case Some(x) => x.toXML("http://www.example.com/IPO","billTo", scope)
          case None => Seq(rt.Helper.nilElem("http://www.example.com/IPO", "billTo", scope))
        },
        via.map(x => x match {
          case Some(x) => x.toXML("http://www.example.com/IPO", "via", scope)
          case None => rt.Helper.nilElem("http://www.example.com/IPO", "via", scope)
        } )).flatten: _*)
  }
}
 
object NillableTest extends rt.ElemNameParser[NillableTest] {
  val targetNamespace = "http://www.example.com/IPO"
 
  def parser(node: scala.xml.Node): Parser[NillableTest] =
    (rt.ElemName(targetNamespace, "price")) ~ 
      opt(rt.ElemName(targetNamespace, "tax")) ~ 
      rep(rt.ElemName(targetNamespace, "tag")) ~ 
      (rt.ElemName(targetNamespace, "shipTo")) ~ 
      opt(rt.ElemName(targetNamespace, "billTo")) ~ 
      rep(rt.ElemName(targetNamespace, "via")) ^^
        { case p1 ~ 
      p2 ~ 
      p3 ~ 
      p4 ~ 
      p5 ~ 
      p6 => NillableTest(if (p1.nil) None
      else Some(p1.text.toInt),
      p2 match {
        case Some(x) => if (x.nil) None
          else Some(x.text.toInt)
        case None    => None
      },
      p3.toList.map(x => if (x.nil) None
      else Some(x.text.toInt) ),
      if (p4.nil) None
      else Some(Addressable.fromXML(p4.node)),
      p5 match {
        case Some(x) => if (x.nil) None
          else Some(Addressable.fromXML(x.node))
        case None    => None
      },
      p6.toList.map(x => if (x.nil) None
      else Some(Addressable.fromXML(x.node)) )) }
}

ここで,tag 要素がオプショナルなシーケンスではなく,オプションのシーケンスを生成したことに注意してください.