user994165
user994165

Reputation: 9512

XML Elements in Any Order, Some Required and Others Aren't

I'm trying to have a list of elements that are allowed in any order. Some of the elements are required (min of 1, max of 1), some are optional with a maximum of one and some are optional with any number. This is what I have and the XSD is valid, but when I go to validate an XML, the rules that I'm trying to implement aren't enforced. For example, id is not made to be required.

<xsd:complexType name="feedType">
        <xsd:annotation>
            <xsd:documentation>
                The Atom feed construct is defined in section 4.1.1 of the format spec.
            </xsd:documentation>
        </xsd:annotation>
        <xsd:choice minOccurs="3" maxOccurs="unbounded">
            <xsd:element name="author" type="atom:personType" minOccurs="0" maxOccurs="unbounded"/>
            <xsd:element name="category" type="atom:categoryType" minOccurs="0" maxOccurs="unbounded"/>
            <xsd:element name="contributor" type="atom:personType" minOccurs="0" maxOccurs="unbounded"/>
            <xsd:element name="generator" type="atom:generatorType" minOccurs="0" maxOccurs="1"/>
            <xsd:element name="icon" type="atom:iconType" minOccurs="0" maxOccurs="1"/>
            <xsd:element name="id" type="atom:idType" minOccurs="1" maxOccurs="1"/>
            <xsd:element name="link" type="atom:linkType" minOccurs="0" maxOccurs="unbounded"/>
            <xsd:element name="logo" type="atom:logoType" minOccurs="0" maxOccurs="1"/>
            <xsd:element name="rights" type="atom:textType" minOccurs="0" maxOccurs="1"/>
            <xsd:element name="subtitle" type="atom:textType" minOccurs="0" maxOccurs="1"/>
            <xsd:element name="title" type="atom:textType" minOccurs="1" maxOccurs="1"/>
            <xsd:element name="updated" type="atom:dateTimeType" minOccurs="1" maxOccurs="1"/>
            <xsd:element name="entry" type="atom:entryType" minOccurs="0" maxOccurs="unbounded"/>
            <xsd:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
        </xsd:choice>
        <xsd:attributeGroup ref="atom:commonAttributes"/>
    </xsd:complexType>

Upvotes: 7

Views: 8320

Answers (3)

Pete Kirkham
Pete Kirkham

Reputation: 49321

Basically, don't do that when using XSD. It is not designed for a use case like this.

You can validate with RelaxNG instead, using its unordered operation, or if you have a partner who requires an XSD then enforce an arbitrary order.

The schema for atom feeds given in RFC4287 uses RelaxNG for this reason:

   atomFeed =
      element atom:feed {
         atomCommonAttributes,
         (atomAuthor*
          & atomCategory*
          & atomContributor*
          & atomGenerator?
          & atomIcon?
          & atomId
          & atomLink*
          & atomLogo?
          & atomRights?
          & atomSubtitle?
          & atomTitle
          & atomUpdated
          & extensionElement*),
         atomEntry*
      }

See Principles of XML design: When the order of XML elements matters for further discussion of when to enforce order in XML

Upvotes: 2

user123444555621
user123444555621

Reputation: 153154

I just ran into the same problem and had a look at what they did in the XHTML XSD. Same situation there inside head: title is required, base is optional, and then there's an arbitrary number of script, style, meta, link and object elements allowed.

Here's what they did: First, they grouped the optional elements that may occur more than once:

  <xs:group name="head.misc">
    <xs:sequence>
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element ref="script"/>
        <xs:element ref="style"/>
        <xs:element ref="meta"/>
        <xs:element ref="link"/>
        <xs:element ref="object"/>
      </xs:choice>
    </xs:sequence>
  </xs:group>

Then they referenced this group in the actual head definition:

  <xs:sequence>
    <xs:group ref="head.misc"/>
    <xs:choice>
      <xs:sequence>
        <xs:element ref="title"/>
        <xs:group ref="head.misc"/>
        <xs:sequence minOccurs="0">
          <xs:element ref="base"/>
          <xs:group ref="head.misc"/>
        </xs:sequence>
      </xs:sequence>
      <xs:sequence>
        <xs:element ref="base"/>
        <xs:group ref="head.misc"/>
        <xs:element ref="title"/>
        <xs:group ref="head.misc"/>
      </xs:sequence>
    </xs:choice>
  </xs:sequence>

This is a bit tricky. Written as regex-like pseudo code, the above looks something like this:

misc=(script|style|meta|link|object)*
head=${misc}(title${misc}(base${misc})?|base${misc}title${misc})

So, technically it works.

However, this requires putting all possible permutations of the mandatory and optional elements (with the misc stuff in between) inside choice. With n elements, that's n! child nodes. So for XHTML with n=2 they ended up with n!=2. In your case with n=8, it's going to be n!=40320.

FWIW, here's the algorithm to generate your XSD:

result = '<xs:group name="misc"><xs:sequence><xs:choice minOccurs="0" maxOccurs="unbounded">'
forall(optionalArbitraryCountElements as element)
    result += '<xs:element ref="' + element.name + '"/>'
result += '</xs:choice></xs:sequence></xs:group>'

result += '<xs:complexType name="feedType"><xs:sequence><xs:group ref="misc"/><xs:choice>'
permutations = getAllPermutations(mandatoryElements + optionalOnceElements)
foreach (permutations as p)
    result += '<xs:sequence>'
    foreach (p as element)
        if (element.isOptional)
            result += '<xs:sequence minOccurs="0">'
        result += '<xs:element ref="' + element.name + '"/><xs:group ref="misc"/>'
        if (element.isOptional)
            result += '</xs:sequence>'
    result += '</xs:sequence>'
result += '</xs:choice></xs:sequence></xs:complexType>'

return result

Upvotes: 13

Mark M
Mark M

Reputation: 976

choice only allows one of its child elements to be present in the XML graph. It looks like you want to use sequence if your elements are always in the same order. If the order is variable then you should use all and wrap all elements that have maxOccurs="unbounded" in a containing list element, because all only allows 1 or zero occurrences of its child elements.

EDIT:

And you should remove the minOccurs & maxOccurs from the choice element. This only allows you to enforce 3 choices, but doesn't allow you specify what choices they are (including repeating the same element multiple times). I don't know what exactly you're trying to enforce, but it won't be effectively enforced that way.

EDIT 2:

You can create list wrappers as follows (using the link element as an example):

<xs:element name="linkList" minOccurs="0" maxOccurs="1">
<xs:complexType>
  <xs:sequence>
    <xs:element name="link" type="atom:linkType" minOccurs="0" maxOccurs="unbounded" />
  </xs:sequence>
</xs:complexType>
</xs:element>

and in the xml document you would (for example) nest your link elements in the linkList:

Old:

<other elements...>
<id>...</id>
<link>...</link>
<link>...</link>
<logo>...</logo>
<other elements...>

New:

<other elements...>
<id>...</id>
<linkList>
  <link>...</link>
  <link>...</link>
</linkList>
<logo>...</logo>
<other elements...>

Upvotes: 5

Related Questions