xsi:nillable
In XML, one can omit an element to express lack of value or use an empty element.
Sometimes it is desirable to represent an unshipped item, unknown information, or inapplicable information explicitly with an element, rather than by an absent element.
The trouble with using an empty element <foo></foo>
is that the emptiness would no longer matches the specified type like xs:positiveInteger
. It is possible to form a xs:union
of xs:positiveInteger
and an xs:enumeration
with only empty string in it to allow either poistive integers or empty string. However, technically speaking, an empty string is different from pure emptiness. In terms of code, it's the difference between null
and ""
, or in Scala, None
and Some("")
.
XML Schema resolves this issue by introducing a special attribute called xsi:nil
. By writing
<price xsi:nil="true" />
XML Schema-aware parsers know that this is a special empty element, which means it's a nil
value, not empty string. Note that the nillability of an element is a separate issue from the cardinality (minOccurs
and maxOccurs
). This means I can define minOccurs=0
and maxOccurs=1
optional element that is also nillable, or even maxOccurs="unbounded"
nillable element, in which case the document could look like this:
<foo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<price xsi:nil="true" />
<price>100</price>
<price xsi:nil="true" />
</foo>
I did not want to introduce null
vs non-null
into generated code more than I have to, so <price xsi:nil="true" />
produces None
, which is the same value as when an element is absent in a minOccurs=0
element.
<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>
The above element produces the following code:
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)) )) } }
Notice that tag
element generates a sequence of options, not an optional sequence.