Validating with XSD
A draft can be well-formed and still be wrong: a requirement that says
"MIGHT", which is not one of the key words of BCP 14; a cross reference
to an anchor that no section has; a draft whose name, written twice,
says -01 in one place and -02 in the other. A schema describes
what a valid document of some kind looks like, so a program can check
its input once, where it comes in, instead of everywhere it uses it. XSD (XML
Schema Definition Language) is the schema language of the W3C, and sxml
implements its current version, XSD 1.1.
The first half of this chapter works in the spec directory, on the
Vane draft and its schema vane.xsd. From Namespaces and Larger
Schemas on, it moves to the scxml directory and
the schemas of the feature flag's state machine, which has namespaces, keys and
assertions of its own.
A schema for the draft
A schema is an XML document in the namespace
http://www.w3.org/2001/XMLSchema. It declares elements and
attributes, and gives each a type. Simple types describe text:
numbers, dates, strings with restrictions. Complex types describe
elements with attributes and children.
vane.xsd describes the part of the xml2rfc vocabulary that the draft
uses. Here is its end, the declaration of the document element rfc:
<!-- The document -->
<xs:element name="rfc">
<xs:complexType>
<xs:sequence>
<xs:element name="front" type="front"/>
<xs:element name="middle">
<xs:complexType>
<xs:sequence>
<xs:element name="section" type="section" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="back" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="references" type="references" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="version" type="xs:integer" fixed="3"/>
<xs:attribute name="category">
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="std"/>
<xs:enumeration value="bcp"/>
<xs:enumeration value="info"/>
<xs:enumeration value="exp"/>
<xs:enumeration value="historic"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="ipr" type="xs:token"/>
<xs:attribute name="submissionType" type="xs:token"/>
<xs:attribute name="docName" type="draft-name"/>
<!-- The draft's name is written twice; XSD 1.1 can check that the
two agree. -->
<xs:assert test="not(@docName) or
@docName = front/seriesInfo[@name = 'Internet-Draft']/@value"/>
</xs:complexType>
<!-- No two anchors are the same, and every xref names an anchor.
xs:unique, not xs:key: most paragraphs have no anchor. -->
<xs:unique name="anchor">
<xs:selector xpath=".//section | .//t | .//dt | .//cref | .//reference | .//references"/>
<xs:field xpath="@anchor"/>
</xs:unique>
<xs:keyref name="xref-target" refer="anchor">
<xs:selector xpath=".//xref"/>
<xs:field xpath="@target"/>
</xs:keyref>
</xs:element>
</xs:schema>
Read from the top:
rfccontains afront, amiddleof one or more sections, and maybe aback, in that order.type="front"andtype="section"name types that are declared earlier in the schema.- Its attributes each have a type:
versionis an integer and must be 3,categoryis one of five words, anddocNamehas the typedraft-name, a string that must look like the name of an Internet-Draft. xs:assertis a rule in XPath that a validrfcmust meet. It is explained below.xs:uniqueandxs:keyrefare rules about values across the whole document: no two anchors are the same, and everyxrefnames one.
The rest of the schema, from the top, declares the types the draft uses. Simple Types and Facets and Complex Types and Content Models read it part by part.
Validating
sxml validate checks documents against the schema. Both drafts are
valid, so it prints nothing, and its exit status is 0:
$ sxml validate --schema=vane.xsd vane.xml vane-00.xml; echo "exit status $?"
exit status 0
To see what an invalid draft looks like, change a copy of the draft
with sed and validate it from standard input, which - names. This
copy has a requirement that says "MIGHT", an xref to a term that
doesn't exist, and a two-digit year:
$ sed -e 's/<bcp14>MUST NOT/<bcp14>MIGHT/' \
-e 's/target="term-percentage"/target="term-percentages"/' \
-e 's/<date year="2026"/<date year="26"/' vane.xml \
| sxml validate --schema=vane.xsd -
-:20:11: cvc-datatype-valid.1.2.1: attribute 'year' has the value '26', which is not valid for type 'xs:gYear'
-:94:37: cvc-enumeration-valid: element 'bcp14' has the value 'MIGHT', which is not valid for type 'keyword': the value is not one of the enumeration facets (XSD 1.1 Part 2, 4.3.5.4); the enumeration facets are 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL', 'SHALL NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', ...
-:92:9: cvc-identity-constraint.4.3: element 'xref': its key sequence is no key sequence of the referenced key (keyref 'xref-target')
sxml reports every error, not only the first. Each one has the line and
column, the name of the rule in the XSD specification that failed (such
as cvc-enumeration-valid), the value, and what was expected. The
xref comes last although it is on line 92: rules across the document,
such as the keyref, are checked at the end of the element they belong
to, here rfc, when every anchor is known. Reading Validation
Errors goes through the messages in detail.
For programs, --format=json writes the same report as JSON:
$ sxml validate --schema=vane.xsd --format=json vane.xml
{"schema":{"documents":["vane.xsd"],"status":"valid"},"files":[{"file":"vane.xml","status":"valid"}]}
Rules across values
The draft's name is written twice: in the docName attribute of rfc,
and in the seriesInfo of the front matter, which is what the rendered
draft shows. Nothing about the structure of the document says that the
two must agree, but XSD 1.1 can say it with an assertion: an XPath
condition on the element, here
<xs:assert test="not(@docName) or
@docName = front/seriesInfo[@name = 'Internet-Draft']/@value"/>
A draft whose docName was updated for -02 while its seriesInfo
was not breaks it:
$ sed 's/docName="draft-example-vane-protocol-01"/docName="draft-example-vane-protocol-02"/' vane.xml \
| sxml validate --schema=vane.xsd -
-:7:1: cvc-assertion: element 'rfc' breaks the assertion 'not(@docName) or @docName = front/ser...' of an anonymous type derived from 'xs:anyType'
The message shows the start of the assertion, so you can find it in the
schema. Inside the test, the context item is the rfc element, with its
attributes and children already validated, so the test can rely on
their types. XSD 1.1 Features has more assertions.
Strict and lax
A reviewer sends notes about a short draft, with the draft inside them:
xsd-review.xml:
<!-- A review: a reviewer's notes around a short draft. The review
vocabulary has no schema; the draft inside it has one. -->
<review by="Grace Hopper">
<note>Is "MIGHT" one of the key words?</note>
<rfc docName="draft-example-vane-brief-00">
<front>
<title>Vane in Brief</title>
<seriesInfo name="Internet-Draft" value="draft-example-vane-brief-00"/>
<author fullname="Ada Byron" surname="Byron"/>
<date year="2026"/>
</front>
<middle>
<section anchor="intro">
<name>Introduction</name>
<t>A service <bcp14>MIGHT</bcp14> refuse a rollout.</t>
</section>
</middle>
</rfc>
</review>
vane.xsd declares no review element. By default, validation is
strict: the document element must have a declaration in the schema,
so the review is invalid. sxml still looks inside it, and validates the
elements it does know:
$ sxml validate --schema=vane.xsd xsd-review.xml
xsd-review.xml:3:1: cvc-elt.1: no global declaration governs element 'review'
xsd-review.xml:15:22: cvc-enumeration-valid: element 'bcp14' has the value 'MIGHT', which is not valid for type 'keyword': the value is not one of the enumeration facets (XSD 1.1 Part 2, 4.3.5.4); the enumeration facets are 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL', 'SHALL NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', ...
With --mode=lax, elements without a declaration are let through, and
the ones with a declaration are validated wherever they are. The review
itself is no longer an error, but the draft inside it still is:
$ sxml validate --mode=lax --schema=vane.xsd xsd-review.xml; echo "exit status $?"
xsd-review.xml:15:22: cvc-enumeration-valid: element 'bcp14' has the value 'MIGHT', which is not valid for type 'keyword': the value is not one of the enumeration facets (XSD 1.1 Part 2, 4.3.5.4); the enumeration facets are 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL', 'SHALL NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', ...
exit status 1
Only the global declarations, the ones directly inside xs:schema,
can be found this way. In vane.xsd that is only rfc: a section
on its own is not validated even in lax mode, because the schema
declares sections only inside other types.
DTDs
Older documents often carry a DTD, the schema language built into XML itself, in a document type declaration at the top. xml2rfc has one too, for authors who prefer it. A DTD can be part of the document, in its internal subset:
xsd-dtd.xml:
<!DOCTYPE cref [
<!ELEMENT cref (#PCDATA)>
<!ATTLIST cref anchor ID #REQUIRED source CDATA #IMPLIED>
]>
<cref source="AM">Should an owner be able to change the duration?</cref>
sxml check --validate-dtd validates a document against its DTD. This
editorial note has no anchor, which its DTD requires:
$ sxml check --validate-dtd xsd-dtd.xml
xsd-dtd.xml:5:1: cvc-complex-type.4: element 'cref' lacks the required attribute 'anchor'
A DTD knows no types beyond names and IDs, and nothing about
namespaces; that is why vocabularies with namespaces and typed values
use XSD. Files and the Outside World shows external DTDs
and entities, and Appendix A has the details of sxml check.
What's in this chapter
The rest of the chapter teaches XSD step by step:
- Simple Types and Facets: built-in types, restrictions, lists and unions;
- Complex Types and Content Models: sequences,
choices,
all, attributes, mixed, simple and empty content, groups; - Deriving Types: extension, restriction,
xsi:type, substitution groups andxsi:nil; - Namespaces and Larger Schemas: target namespaces, import, wildcards and schema hints, on the flag's state machine;
- Keys and References:
xs:unique,xs:keyandxs:keyref; - XSD 1.1 Features: assertions, conditional types, open content, default attributes and overrides;
- Reading Validation Errors: what the messages mean, and how to fix what they report;
- Typed Data and Schema-Aware Queries: using the types in XQuery and XSLT.
A schema is also a good source for code: Project: Generating Vane's SDKs from a Schema turns one into TypeScript types and runtime checks.