Data Formats (JSON, YAML, TOML, XML, CSV) Module 7 – XML 1.0: syntax, namespaces, parsing, security, schemas and signatures
Validating XML with XSD 1.0 and 1.1
Write XSD simple and complex types, check references with keys and keyrefs, see what XSD 1.1 assertions add, and read the DTDs of older formats.
What you will learn
- Write XSD simple types with facets and complex types with sequence, choice and all
- Check unique values and references inside a document with xs:key and xs:keyref
- Write an XSD 1.1 assertion for a rule that compares values, and check that rule where only XSD 1.0 runs
- Read the declarations of a DTD, and choose between XSD, RELAX NG and Schematron for a format
Before you start
What you’ll learn · Before you start
What you will learn
- Write XSD simple types with facets and complex types with sequence, choice and all
- Check unique values and references inside a document with xs:key and xs:keyref
- Write an XSD 1.1 assertion for a rule that compares values, and check that rule where only XSD 1.0 runs
- Read the declarations of a DTD, and choose between XSD, RELAX NG and Schematron for a format
Before you start
On this page
An XML parser reads this lesson’s broken.xml without a complaint, yet the bill in it holds five mistakes: a bill number three digits short, a day its month does not have, a rate with three decimal places, units written as words, and a due date where a paid date already stands. An XML Schema, or XSD, says what a correct bill is, part by part and value by value, and a validator reports every place a document breaks it.
You will write simple types with facets and complex types with sequence, choice and all, check references with keys, see what XSD 1.1 assertions add and why the JDK refuses them, and read the DTDs of older formats. Every result below comes from the validator built into Java 25, or from Python.
A schema says what a correct bill looks like
The bill is synthetic, and so are its tariff’s rates: one consumer, a billing period, two meter readings, three tariff
slabs, a charge for each slab and a due date. Its schema, in the second tab, declares those parts in the namespace
urn:example:bill. targetNamespace names it, and elementFormDefault="qualified" puts the local elements in it too,
so the bill’s default namespace declaration must name the same URI, as the namespaces lesson showed:
bill/bill.xml
<?xml version="1.0" encoding="UTF-8"?>
<bill xmlns="urn:example:bill" number="B-2026-000123">
<consumer number="MH0123456789">
<name>Asha Patil</name>
</consumer>
<period from="2026-09-01" to="2026-09-30"/>
<readings>
<reading on="2026-09-01">10432</reading>
<reading on="2026-09-30">10781</reading>
</readings>
<tariff>
<slab id="s1" upTo="100" rate="4.71"/>
<slab id="s2" upTo="300" rate="10.29"/>
<slab id="s3" rate="14.55"/>
</tariff>
<charges>
<charge slab="s1" units="100" amount="471.00"/>
<charge slab="s2" units="200" amount="2058.00"/>
<charge slab="s3" units="49" amount="712.95"/>
</charges>
<due>2026-10-21</due>
</bill> bill/bill.xsd
<?xml version="1.0" encoding="UTF-8"?>
<!-- The schema of a synthetic electricity bill: its parts, their values
and the references between them. XSD 1.0. -->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:b="urn:example:bill"
targetNamespace="urn:example:bill"
elementFormDefault="qualified">
<xs:element name="bill" type="b:Bill">
<xs:key name="slabId">
<xs:selector xpath="b:tariff/b:slab"/>
<xs:field xpath="@id"/>
</xs:key>
<xs:keyref name="chargeSlab" refer="b:slabId">
<xs:selector xpath="b:charges/b:charge"/>
<xs:field xpath="@slab"/>
</xs:keyref>
</xs:element>
<xs:complexType name="Bill">
<xs:sequence>
<xs:element name="consumer" type="b:Consumer"/>
<xs:element name="period" type="b:Period"/>
<xs:element name="readings" type="b:Readings"/>
<xs:element name="tariff" type="b:Tariff"/>
<xs:element name="charges" type="b:Charges"/>
<xs:choice>
<xs:element name="due" type="xs:date"/>
<xs:element name="paid" type="xs:date"/>
</xs:choice>
</xs:sequence>
<xs:attribute name="number" type="b:BillNumber" use="required"/>
</xs:complexType>
<xs:complexType name="Consumer">
<xs:all>
<xs:element name="name" type="xs:string"/>
<xs:element name="phone" type="b:Phone" minOccurs="0"/>
</xs:all>
<xs:attribute name="number" type="b:ConsumerNumber" use="required"/>
</xs:complexType>
<xs:complexType name="Period">
<xs:attribute name="from" type="xs:date" use="required"/>
<xs:attribute name="to" type="xs:date" use="required"/>
</xs:complexType>
<xs:complexType name="Readings">
<xs:sequence>
<xs:element name="reading" minOccurs="2" maxOccurs="12">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:nonNegativeInteger">
<xs:attribute name="on" type="xs:date" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="Tariff">
<xs:sequence>
<xs:element name="slab" maxOccurs="unbounded">
<xs:complexType>
<xs:attribute name="id" type="xs:NCName" use="required"/>
<xs:attribute name="upTo" type="xs:positiveInteger"/>
<xs:attribute name="rate" type="b:Rate" use="required"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="Charges">
<xs:sequence>
<xs:element name="charge" maxOccurs="unbounded">
<xs:complexType>
<xs:attribute name="slab" type="xs:NCName" use="required"/>
<xs:attribute name="units" type="xs:nonNegativeInteger" use="required"/>
<xs:attribute name="amount" type="b:Money" use="required"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:simpleType name="BillNumber">
<xs:restriction base="xs:string">
<xs:pattern value="B-[0-9]{4}-[0-9]{6}"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ConsumerNumber">
<xs:restriction base="xs:string">
<xs:pattern value="[A-Z]{2}[0-9]{10}"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Phone">
<xs:restriction base="xs:string">
<xs:pattern value="[0-9]{10}"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Rate">
<xs:restriction base="xs:decimal">
<xs:minInclusive value="0"/>
<xs:totalDigits value="4"/>
<xs:fractionDigits value="2"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Money">
<xs:restriction base="xs:decimal">
<xs:minInclusive value="0"/>
<xs:fractionDigits value="2"/>
</xs:restriction>
</xs:simpleType>
</xs:schema> Read the schema from the top. The global element bill has the type Bill, a sequence of five elements and a choice;
each part has a type of its own further down, and the simple types at the end, such as Rate, say which text each
value may hold. The xs:key and xs:keyref inside bill say which values must be unique and which must point at
them. A validator applies all of it in one pass, in the order it meets the document. Step through that pass over
broken.xml, a copy of the bill with five mistakes:
The drawing shows the whole scene; its 9 steps are listed below.
Every step as text
- One pass with the parser. The validator checks each part of broken.xml as the parser reads it, and the parser finds no syntax error. A syntax error would be fatal: the pass would end there, after reporting the problems found before it.
- The root's declaration. The root is bill in the namespace urn:example:bill, and bill.xsd declares a global element with that expanded name and the type Bill. Without the namespace, the validator finds no declaration for the root and checks nothing below it.
- Line 2, a pattern. The number attribute, B-2026-124, has three digits after the year where the BillNumber pattern asks for six, so the rule cvc-pattern-valid fails.
- Line 6, a date. The period ends on day 31 of a month that has 30 days. An xs:date is checked against the calendar, not only against the shape of the text, so cvc-datatype-valid fails.
- Line 13, digits. The rate 10.295 has three digits after the point, and the Rate type allows two, so cvc-fractionDigits-valid fails. The validator names the first facet that fails; the value also has five digits, one more than totalDigits allows.
- Line 17, not a number. The units attribute holds the words two hundred. Its type, xs:nonNegativeInteger, accepts only digits with an optional sign, so cvc-datatype-valid fails again.
- Line 20, one child too many. The sequence of Bill ends with a choice of due or paid. paid took that place, so due has nowhere to go: cvc-complex-type.2.4.d, no child element is expected at this point.
- Keys at the end. Each slab id was collected as it was read, and a repeated one would have been reported at its own line. When the root ends, each charge's slab is looked up among them: s1 and s2 both exist, so nothing fails here.
- Five problems, one pass. The error handler collected five problems, each with its line and the name of the rule it breaks, and the document is invalid. A handler that throws at the first error would have reported line 2 only.
The program below loads bill.xsd once, with external access switched off as the safe-parsing lesson recommends, and validates four documents with an error handler that keeps every problem instead of stopping at the first:
import java.io.File;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXParseException;
/** Validates four bills against bill.xsd and prints every problem with its line. */
public class Validate {
public static void main(String[] args) throws Exception {
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
Schema schema = factory.newSchema(new File("bill.xsd"));
Map<String, String> bills = new LinkedHashMap<>();
bills.put("bill.xml", Files.readString(Path.of("bill.xml")));
bills.put("broken.xml", Files.readString(Path.of("broken.xml")));
bills.put("bill.xml without its xmlns", bills.get("bill.xml").replace(" xmlns=\"urn:example:bill\"", ""));
bills.put("broken.xml, </charges> mistyped", bills.get("broken.xml").replace("</charges>", "</charge>"));
for (var bill : bills.entrySet()) {
List<String> problems = new ArrayList<>();
Validator validator = schema.newValidator();
validator.setErrorHandler(new ErrorHandler() {
public void warning(SAXParseException e) {
problems.add("warning, line " + e.getLineNumber() + ": " + e.getMessage());
}
public void error(SAXParseException e) {
// Xerces follows each bad value with a general message (cvc-attribute.3, cvc-type.3.1.3);
// the first message names the rule that failed, so the general one is left out.
if (!e.getMessage().startsWith("cvc-attribute.3") && !e.getMessage().startsWith("cvc-type.3.1.3")) {
problems.add("line " + e.getLineNumber() + ": " + e.getMessage());
}
}
public void fatalError(SAXParseException e) throws SAXParseException {
problems.add("line " + e.getLineNumber() + ", fatal: " + e.getMessage());
throw e; // not well-formed: the text is not XML, so the pass ends here
}
});
boolean stopped = false;
try {
validator.validate(new StreamSource(new StringReader(bill.getValue())));
} catch (SAXParseException e) {
stopped = true; // the handler has recorded it
}
int n = problems.size();
System.out.println(bill.getKey() + ": " + (n == 0 ? "valid" : n + (n == 1 ? " problem" : " problems"))
+ (stopped ? ", the last one fatal" : ""));
problems.forEach(p -> System.out.println(" " + p));
}
}
} Output
bill.xml: valid
broken.xml: 5 problems
line 2: cvc-pattern-valid: Value 'B-2026-124' is not facet-valid with respect to pattern 'B-[0-9]{4}-[0-9]{6}' for type 'BillNumber'.
line 6: cvc-datatype-valid.1.2.1: '2026-09-31' is not a valid value for 'date'.
line 13: cvc-fractionDigits-valid: Value '10.295' has 3 fraction digits, but the number of fraction digits has been limited to 2.
line 17: cvc-datatype-valid.1.2.1: 'two hundred' is not a valid value for 'integer'.
line 20: cvc-complex-type.2.4.d: Invalid content was found starting with element 'due'. No child element is expected at this point.
bill.xml without its xmlns: 1 problem
line 2: cvc-elt.1.a: Cannot find the declaration of element 'bill'.
broken.xml, </charges> mistyped: 5 problems, the last one fatal
line 2: cvc-pattern-valid: Value 'B-2026-124' is not facet-valid with respect to pattern 'B-[0-9]{4}-[0-9]{6}' for type 'BillNumber'.
line 6: cvc-datatype-valid.1.2.1: '2026-09-31' is not a valid value for 'date'.
line 13: cvc-fractionDigits-valid: Value '10.295' has 3 fraction digits, but the number of fraction digits has been limited to 2.
line 17: cvc-datatype-valid.1.2.1: 'two hundred' is not a valid value for 'integer'.
line 18, fatal: The element type "charges" must be terminated by the matching end-tag "</charges>".
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Validate.java
bill.xml is valid. broken.xml has the five problems of the figure, in document order, each with its line and the name
of the rule it breaks: cvc-pattern-valid and cvc-fractionDigits-valid are the ids the XSD specification gives its
validation rules, so each message can be looked up there. The third document is bill.xml without its xmlns. Its root
is now bill in no namespace, which bill.xsd does not declare, so the validator reports cvc-elt.1.a and checks
nothing below the root. The fourth is broken.xml with its </charges> end tag mistyped at line 18. Its first four
problems were still reported, because the validator checks each part as the parser reads it; the syntax error is fatal
and ends the pass there, so the due at line 20 was never checked.
Validating without an error handler
Wrong: calling validator.validate(source) and nothing else. With no handler, the JDK behaves as if one throws at
the first error, so broken.xml would report line 2 and hide the four problems after it. Right: set a handler that
collects every error, as Validate.java does, and rethrow only fatal errors, which mean the text is not XML at all.
Checkpoint
Check: What a validator checks, and in which order
Simple types narrow a built-in type with facets
Every value in the bill has a simple type: In XML Schema, a type for text with no child elements, such as xs:decimal or xs:date, or a type derived from one by facets. Attributes always have simple types; elements may have them too.: text with no child elements, checked
against a built-in type of XSD Part 2 or a type derived from one. The rate attribute uses Rate, an xs:decimal
narrowed by three facet: A rule that narrows an XML Schema simple type, such as pattern, enumeration, maxLength, minInclusive, totalDigits or fractionDigits. A value must satisfy every facet of its type and of the types it was derived from.: minInclusive 0, totalDigits 4 and fractionDigits 2. A value
must satisfy every facet of its type and of each type that type was derived from. The program sends single values to
the JDK’s validator, through a small schema that includes bill.xsd:
import java.io.File;
import java.io.StringReader;
import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.xml.sax.SAXParseException;
/** Checks single values against two simple types of bill.xsd, and against a pattern written with ^ and $. */
public class Facets {
// A small schema that includes bill.xsd and declares one element for each type to try.
static final String TYPES = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:b="urn:example:bill"
targetNamespace="urn:example:bill" elementFormDefault="qualified">
<xs:include schemaLocation="bill.xsd"/>
<xs:element name="rate" type="b:Rate"/>
<xs:element name="consumer" type="b:ConsumerNumber"/>
<xs:element name="carets">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="^[A-Z]{2}[0-9]{10}$"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
</xs:schema>""";
public static void main(String[] args) throws Exception {
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Schema schema = factory.newSchema(new StreamSource(new StringReader(TYPES), new File("types.xsd").toURI().toString()));
String[][] cases = {
{"rate", "4.71"}, {"rate", "04.710"}, {"rate", " 4.71 "}, {"rate", "10.295"},
{"rate", "123.4"}, {"rate", "123.45"}, {"rate", "-1"}, {"rate", "1e2"}, {"rate", "4,71"},
{"consumer", "MH0123456789"}, {"consumer", "mh0123456789"},
{"consumer", "MH01234567890"}, {"consumer", " MH0123456789"},
{"carets", "MH0123456789"}, {"carets", "^MH0123456789$"},
};
for (String[] c : cases) {
String doc = "<" + c[0] + " xmlns=\"urn:example:bill\">" + c[1] + "</" + c[0] + ">";
String verdict = "valid";
try {
schema.newValidator().validate(new StreamSource(new StringReader(doc)));
} catch (SAXParseException e) {
verdict = e.getMessage().substring(0, e.getMessage().indexOf(':')); // the rule's name
}
System.out.printf("%-9s %-17s %s%n", c[0], "[" + c[1] + "]", verdict);
}
}
} Output
rate [4.71] valid rate [04.710] valid rate [ 4.71 ] valid rate [10.295] cvc-fractionDigits-valid rate [123.4] valid rate [123.45] cvc-totalDigits-valid rate [-1] cvc-minInclusive-valid rate [1e2] cvc-datatype-valid.1.2.1 rate [4,71] cvc-datatype-valid.1.2.1 consumer [MH0123456789] valid consumer [mh0123456789] cvc-pattern-valid consumer [MH01234567890] cvc-pattern-valid consumer [ MH0123456789] cvc-pattern-valid carets [MH0123456789] cvc-pattern-valid carets [^MH0123456789$] valid
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Facets.java
Three of the results surprise anyone who reads values as text:
04.710and4.71are valid.xs:decimalcollapses white space before anything else, and its digit facets count the digits of the value 4.71, not of the text: the specification definestotalDigitson the value and lets extra leading and trailing zeros through.123.4is valid and123.45is not. Four digits in all are allowed and five are not, wherever the point falls.10.295failsfractionDigits, and it also has five digits. The validator names the first facet that fails; a value can break several at once.
Key idea
Facets constrain values, not text: the type decides whether white space is collapsed or kept, and digit facets count the digits of the value, 4.71, not of a text such as 04.710.
The facets and built-in types that matter most for data like the bill:
| Type or facet | What it checks | In this lesson |
|---|---|---|
xs:decimal |
an exact number: digits 0 to 9, one optional point, an optional sign, no exponent | 1e2 and 4,71 refused |
xs:double |
a binary floating-point number, patterned after IEEE double precision | use it for measurements, not money |
xs:date |
a real calendar day, with an optional time zone | the 31st day of September refused |
pattern |
the whole value against an XSD regular expression | MH01234567890 refused |
enumeration |
one value of a fixed list | the DTD’s main or solar |
minInclusive, maxInclusive |
bounds on the value, with minExclusive and maxExclusive |
-1 refused |
totalDigits, fractionDigits |
the digits of a decimal value, all and after the point | 123.45 and 10.295 refused |
length, minLength, maxLength |
the number of characters, or of items in a list type | none in the bill |
Patterns need the most care, because they look like the regular expressions of programming languages and are not read
like them. The specification anchors every XSD pattern at both ends, so [A-Z]{2}[0-9]{10} refused MH01234567890,
which has one digit too many. The carets rows show the other side: ^ and $ are ordinary characters in XSD, so
^[A-Z]{2}[0-9]{10}$ refused the plain consumer number and accepted it only between a caret and a dollar sign. This
Python program tests the same pattern the two ways people test patterns:
"""An XSD pattern must match the whole value. Python's re.search is happy with a match anywhere."""
import re
PATTERN = "[A-Z]{2}[0-9]{10}" # ConsumerNumber in bill.xsd
VALUES = ["MH0123456789", "XMH0123456789", "MH01234567890", "mh0123456789", "Ref MH0123456789 paid"]
print(f"{'value':23} {'re.search':11} re.fullmatch, as XSD reads it")
for value in VALUES:
anywhere = "matches" if re.search(PATTERN, value) else "-"
whole = "matches" if re.fullmatch(PATTERN, value) else "-"
print(f"{value:23} {anywhere:11} {whole}") Output
value re.search re.fullmatch, as XSD reads it MH0123456789 matches matches XMH0123456789 matches - MH01234567890 matches - mh0123456789 - - Ref MH0123456789 paid matches -
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 anchored.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
re.search found the pattern in four of the five values; re.fullmatch, which reads a pattern the way XSD does,
accepted only MH0123456789. Press Edit and add a value with a space in front: xs:string keeps white space, so the
schema refuses it as well, and so does fullmatch.
Testing a schema pattern with re.search, or adding ^ and $
Wrong: checking a new pattern with re.search, then writing it into the schema as ^[0-9]{6}$. The test passes
values the schema will refuse, and the carets make the schema demand two characters no value has. Right: test with
re.fullmatch and write the pattern without anchors, [0-9]{6}. Python’s \d, like XSD’s own \d, also matches
other scripts’ digits; [0-9] matches only 0 to 9.
Checkpoint
Check: Simple types and their facets
Sequence, choice and all decide the order and number of children
An element with attributes, child elements or both has a complex type: In XML Schema, the type of an element that has attributes, child elements or both. Its content model, built from sequence, choice and all groups, says which children may appear, in which order and how often.. Its content
model is built from three groups: xs:sequence, the children in this order; xs:choice, one of these; and xs:all,
each of these in any order. Every particle has minOccurs and maxOccurs, both 1 unless the schema says otherwise.
The Bill type is a sequence of five elements and a choice, Consumer is an all group, and Readings allows 2 to 12
reading elements. Step through the children of broken.xml as the validator matches them against the Bill sequence:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- consumer. The first child is consumer, the first particle of the sequence, so it fits. Inside it, xs:all lets name and the optional phone come in either order, each at most once.
- period and readings. period fills the second place, and readings the third. readings holds two reading elements, inside its range of 2 to 12.
- tariff and charges. tariff and charges follow in the order the schema gives. Had charges come first, the validator would have said what it expected instead: cvc-complex-type.2.4.a, one of tariff is expected.
- The choice. paid takes the last place in the sequence, the choice of due or paid. With it, the content of bill is complete.
- One child too many. due arrives after paid, but no particle of the sequence is left for it. The validator reports cvc-complex-type.2.4.d at line 20: no child element is expected at this point.
The last child, due, finds no place, because the choice was already filled by paid. Four copies of bill.xml, each
with one mistake of order or number, show the other messages you will meet most often:
import java.io.File;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.xml.sax.SAXParseException;
/** Four copies of bill.xml, each with one mistake in the order or the number of its elements. */
public class Order {
public static void main(String[] args) throws Exception {
String bill = Files.readString(Path.of("bill.xml"));
String tariff = bill.substring(bill.indexOf(" <tariff>"), bill.indexOf(" <charges>"));
String reading = " <reading on=\"2026-09-30\">10781</reading>\n";
Map<String, String> copies = new LinkedHashMap<>();
copies.put("charges before tariff", bill.replace(tariff, "").replace(" <due>", tariff + " <due>"));
copies.put("one reading", bill.replace(reading, ""));
copies.put("thirteen readings", bill.replace(reading, reading.repeat(12)));
copies.put("name twice", bill.replace("<name>Asha Patil</name>", "<name>Asha Patil</name><name>A. Patil</name>"));
Schema schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(new File("bill.xsd"));
for (var copy : copies.entrySet()) {
try {
schema.newValidator().validate(new StreamSource(new StringReader(copy.getValue())));
System.out.println(copy.getKey() + ": valid");
} catch (SAXParseException e) {
System.out.println(copy.getKey() + ", line " + e.getLineNumber() + ":\n " + e.getMessage());
}
}
}
} Output
charges before tariff, line 11:
cvc-complex-type.2.4.a: Invalid content was found starting with element '{"urn:example:bill":charges}'. One of '{"urn:example:bill":tariff}' is expected.
one reading, line 9:
cvc-complex-type.2.4.i: The content of element 'readings' is not complete. '"urn:example:bill":reading' is expected to occur a minimum of '2' times. One more instance is required to satisfy this constraint.
thirteen readings, line 20:
cvc-complex-type.2.4.f: '"urn:example:bill":reading' can occur a maximum of '12' times in the current sequence. This limit was exceeded. No child element is expected at this point.
name twice, line 4:
cvc-complex-type.2.4.a: Invalid content was found starting with element '{"urn:example:bill":name}'. One of '{"urn:example:bill":phone}' is expected.
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Order.java
Read each message by its code. 2.4.a means an element arrived where the model expected another, and the message
lists what it expected, tariff here. 2.4.i means the content ended too early, with one reading where two are
needed; 2.4.f means a particle occurred more often than its maxOccurs, as the thirteen readings did; 2.4.d, from
the figure, means nothing at all may follow. The names in curly braces are expanded names: the namespace, then the
local name. The all group is the easy one to misread. A second name is reported as 2.4.a with phone expected,
because in XSD 1.0 an all group takes each of its elements at most once, so after name only phone may come.
Two shapes of complex type have no content model of children. period and slab carry attributes only, and
reading holds a number with an on attribute: xs:simpleContent extends xs:nonNegativeInteger with that
attribute, so the text inside is still checked as a number.
Order that does not matter
To accept notes and adjustments in any order and any number, use xs:choice with maxOccurs="unbounded". In XSD 1.0
xs:all frees the order but not the count.
Checkpoint
Check: Sequence, choice and all
Keys and keyrefs check references inside one document
The charges of the bill refer to slabs by id: slab="s3" must name a slab of the tariff. Two
identity constraint: An XML Schema rule on an element, xs:unique, xs:key or xs:keyref, that picks elements with a selector, reads values from each with fields, and checks that the values are unique or point at existing keys. on the element bill check that. The key slabId says each
slab’s @id is present and unique, and the keyref chargeSlab says each charge’s @slab is one of those key values.
Each has a selector, an XPath that picks elements below bill, and one or more fields, XPaths that read a value from
each picked element. The values are compared as typed values, so in a decimal field 1.0 and 1 would be the same
key. XSD 1.0 allows only a small part of XPath here: child steps, an optional .// at the start, @ for the last step
of a field, and | between paths. Step through the check of a bill whose slab s3 was renamed s2:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Collect the keys. The selector of slabId, b:tariff/b:slab, picks the three slabs below bill, and its field, @id, reads one value from each: s1, s2 and s2.
- A duplicate. Two slabs give the key value s2, so the key fails at line 14 with cvc-identity-constraint.4.2.2, a duplicate key value. A key must be unique and present on every selected element.
- Collect the references. The selector of chargeSlab, b:charges/b:charge, picks the three charges, and @slab reads s1, s2 and s3 from them.
- Two references resolve. s1 and s2 are values of the key, so the first two charges point at slabs that exist.
- One points at nothing. No slab has the key s3 any more, so the keyref fails at line 22 with cvc-identity-constraint.4.3: the key value s3 is not found.
- Without the prefix. Now the same schema with the selectors tariff/slab and charges/charge. In XPath 1.0 a name without a prefix means no namespace, so they select no element of urn:example:bill: nothing is collected, nothing is compared, and the same bill is valid.
Keys.java runs both checks on bill.xml and on the renamed copy, then repeats the second run with the selectors of the figure’s last step:
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXParseException;
/** The key and keyref of bill.xsd at work, then the same constraints with b: left out of their selectors. */
public class Keys {
public static void main(String[] args) throws Exception {
String xsd = Files.readString(Path.of("bill.xsd"));
String bill = Files.readString(Path.of("bill.xml"));
// Slab s3 renamed s2: two slabs now share an id, and the charge for s3 points at no slab.
String broken = bill.replace("<slab id=\"s3\"", "<slab id=\"s2\"");
String unprefixed = xsd.replace("xpath=\"b:tariff/b:slab\"", "xpath=\"tariff/slab\"")
.replace("xpath=\"b:charges/b:charge\"", "xpath=\"charges/charge\"");
check("bill.xsd, bill.xml", xsd, bill);
check("bill.xsd, s3 renamed s2", xsd, broken);
check("selectors without b:, s3 renamed s2", unprefixed, broken);
}
static void check(String label, String xsd, String doc) throws Exception {
Schema schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
.newSchema(new StreamSource(new StringReader(xsd)));
List<String> problems = new ArrayList<>();
Validator validator = schema.newValidator();
validator.setErrorHandler(new ErrorHandler() {
public void warning(SAXParseException e) { problems.add(e.getMessage()); }
public void error(SAXParseException e) { problems.add("line " + e.getLineNumber() + ": " + e.getMessage()); }
public void fatalError(SAXParseException e) throws SAXParseException { throw e; }
});
validator.validate(new StreamSource(new StringReader(doc)));
System.out.println(label + ": " + (problems.isEmpty() ? "valid" : problems.size() + " problems"));
problems.forEach(p -> System.out.println(" " + p));
}
} Output
bill.xsd, bill.xml: valid bill.xsd, s3 renamed s2: 2 problems line 14: cvc-identity-constraint.4.2.2: Duplicate key value [s2] declared for identity constraint "slabId" of element "bill". line 22: cvc-identity-constraint.4.3: Key 'chargeSlab' with value 's3' not found for identity constraint of element 'bill'. selectors without b:, s3 renamed s2: valid
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Keys.java
The renamed slab gives two errors: a duplicate key at line 14 and, at line 22, a reference to s3 that no key matches.
The third run is the dangerous one. With tariff/slab in place of b:tariff/b:slab, the same broken bill is valid.
XPath 1.0 reads a name without a prefix as a name in no namespace and never applies a default namespace, so in a
schema with a target namespace the selector picks nothing, and a constraint over nothing always holds.
Use xs:unique where the field may be missing: it forbids duplicates among the values that are present, while
xs:key also demands a value on every element its selector picks. Keys, uniques and keyrefs all work inside one
document; a reference to something outside it, such as a product list in a database, is a check for code.
Key idea
Write the target namespace’s prefix into every step of a selector and a field, and keep a test document with a broken reference that validation must refuse: a constraint whose selector picks nothing passes every document.
MySmartCoPilot’s XSD validator flags selectors of this kind, and it can also write a starter schema from a sample document. Tighten such a schema by hand: a sample cannot show a limit such as at most 12 readings, or which values must be unique.
XSD Validator Validate your own XML against an XSD in your browser, or generate a starter schema from a sample document.Checkpoint
Check: Keys and keyrefs
XSD 1.1 assertions compare values that XSD 1.0 cannot
Two rules of a correct bill are out of reach of everything above. A billing period must not end before it starts, and the units charged must add up to the units the meter recorded: 10781 − 10432 = 349, charged as 100 + 200 + 49. Each rule compares one value with another, while XSD 1.0 checks every value on its own, so a bill with its period reversed, or with 59 units charged where the meter recorded 49, is still valid against bill.xsd: the program of the next section validates both.
XSD 1.1 adds assertion (XSD 1.1): An xs:assert element on an XSD 1.1 type: an XPath 2.0 test over the element and its descendants, with typed values, that must be true for the element to be valid. XSD 1.0 has no assertions. for such rules. An xs:assert element on a complex type holds
an XPath 2.0 test, and an element of that type is valid only if the test is true. On the Bill type, the two rules
read:
<xs:assert test="b:period/@from le b:period/@to"/><xs:assert test="sum(b:charges/b:charge/@units) eq b:readings/b:reading[last()] - b:readings/b:reading[1]"/>
The tests compare typed values. For an assertion, the validator builds an XPath data model whose root is the element
being checked, with its descendants typed as the schema declares them, so le compares two xs:date values and
sum() adds numbers. Nothing outside that element is in the model: an assertion on the type of charges could not see
the readings. Put each rule on the type of the smallest element that holds every value it compares.
XSD 1.1 brings more than assertions: conditional type assignment chooses an element’s type from its attributes, open
content lets a type accept elements that a later version adds, all groups may repeat elements and hold wildcards, and
xpathDefaultNamespace gives selectors a default namespace. Support is the catch:
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.SchemaFactory;
import org.xml.sax.SAXException;
/** Adds two XSD 1.1 assertions to the Bill type of bill.xsd and asks the JDK to load the result. */
public class Assert11 {
public static void main(String[] args) throws Exception {
String xsd = Files.readString(Path.of("bill.xsd"));
String attribute = "<xs:attribute name=\"number\" type=\"b:BillNumber\" use=\"required\"/>";
String asserts = attribute + """
<xs:assert test="b:period/@from le b:period/@to"/>
<xs:assert test="sum(b:charges/b:charge/@units)
eq b:readings/b:reading[last()] - b:readings/b:reading[1]"/>""";
String version11 = xsd.replace(attribute, asserts);
try {
SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
.newSchema(new StreamSource(new StringReader(version11)));
System.out.println("XSD 1.0 factory: loaded");
} catch (SAXException e) {
System.out.println("XSD 1.0 factory: " + e.getMessage());
}
// Two more schema languages, XSD 1.1 and RELAX NG: does the JDK install a validator for either?
for (String language : new String[] {"http://www.w3.org/XML/XMLSchema/v1.1", XMLConstants.RELAXNG_NS_URI}) {
try {
SchemaFactory.newInstance(language);
System.out.println(language + ": found");
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
}
} Output
XSD 1.0 factory: s4s-elt-invalid-content.1: The content of 'Bill' is invalid. Element 'assert' is invalid, misplaced, or occurs too often. No SchemaFactory that implements the schema language specified by: http://www.w3.org/XML/XMLSchema/v1.1 could be loaded No SchemaFactory that implements the schema language specified by: http://relaxng.org/ns/structure/1.0 could be loaded
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Assert11.java
The JDK refused the schema, because assert is not an element of the XSD 1.0 language, and asking for the XSD 1.1
language found no implementation at all: the SchemaFactory documentation requires only XML Schema 1.0. XSD 1.1
validation exists in Saxon’s Enterprise edition, in a separate XSD 1.1 package of Apache Xerces2 Java and in the Python
library xmlschema, so check which validator every party runs before you publish a 1.1 schema. The schema languages of
this lesson, in the order they appeared:
- 1998
XML 1.0 and its DTDs structure
The first edition of XML 1.0 defines document type declarations: element content models, attribute lists with a few types such as CDATA, ID and NMTOKEN, and entities. A DTD has no data types and knows nothing of namespaces.
- 2001
XML Schema 1.0 types
XML Schema becomes a W3C Recommendation in two normative parts, Structures and Datatypes: typed values such as xs:decimal and xs:date, facets, namespaces, and keys and keyrefs. RELAX NG is published at OASIS the same year.
- 2003
RELAX NG at ISO structure
RELAX NG becomes ISO/IEC 19757-2: a grammar of patterns for elements and attributes, which can use the XML Schema datatypes. An amendment later adds its compact, non-XML syntax.
- 2004
XML Schema 1.0, Second Edition types
A second edition folds in the corrections of the first, with no new features. XML Schema 1.0 is the one schema language the JDK's SchemaFactory is required to support.
- 2012
XML Schema 1.1 rules
XSD 1.1 adds assertions in XPath 2.0, conditional type assignment, open content and freer all groups. Support is narrower than for 1.0: the JDK has none, Saxon has it in its Enterprise edition only, and Xerces in a separate package.
- 2025
ISO Schematron, fourth edition rules
Schematron, standardised as ISO/IEC 19757-3, states rules as XPath assertions with messages for people. Its fourth edition, twenty years after the first, adds a binding for XQuery 3.1 and new elements such as group and library.
The timeline has 6 events, from 1998 to 2025.
Checkpoint
Check: XSD 1.1 assertions
DTDs, RELAX NG and Schematron fill different gaps
Where XSD 1.1 cannot run, state the rules that compare values on their own. Schematron: A rule language for XML, standardised as ISO/IEC 19757-3: each rule has a context and asserts, and each assert is an XPath test with a message for people, which suits rules that compare values. is made for that: a schema of rules, each with a context and asserts, and each assert an XPath test with a message for people. bill-rules.sch holds the two rules of the last section in XPath 1.0, which has no date type, so it compares the period’s dates as numbers with the hyphens taken out:
bill/bill-rules.sch
<?xml version="1.0" encoding="UTF-8"?>
<!-- Two rules that XSD 1.0 cannot state: each compares values with each other.
ISO Schematron, with XPath 1.0 tests. -->
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt">
<sch:ns prefix="b" uri="urn:example:bill"/>
<sch:pattern>
<sch:rule context="b:bill">
<sch:assert test="translate(b:period/@from, '-', '') <= translate(b:period/@to, '-', '')">
The billing period ends before it starts.
</sch:assert>
<sch:assert test="sum(b:charges/b:charge/@units)
= b:readings/b:reading[last()] - b:readings/b:reading[1]">
The units charged differ from the units the meter recorded.
</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema> bill/Rules.java
import java.io.File;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
/** Validates three bills against bill.xsd, then runs the asserts of bill-rules.sch with the JDK's XPath 1.0. */
public class Rules {
static final String SCH = "http://purl.oclc.org/dsdl/schematron";
public static void main(String[] args) throws Exception {
String bill = Files.readString(Path.of("bill.xml"));
Map<String, String> bills = new LinkedHashMap<>();
bills.put("bill.xml", bill);
bills.put("period reversed", bill.replace("from=\"2026-09-01\" to=\"2026-09-30\"", "from=\"2026-09-30\" to=\"2026-09-01\""));
bills.put("49 units charged as 59", bill.replace("units=\"49\"", "units=\"59\""));
Schema schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(new File("bill.xsd"));
Document rules = parse(Files.readString(Path.of("bill-rules.sch")));
XPath xpath = XPathFactory.newInstance().newXPath();
xpath.setNamespaceContext(prefixes(rules));
for (var entry : bills.entrySet()) {
schema.newValidator().validate(new StreamSource(new StringReader(entry.getValue()))); // throws if invalid
Document doc = parse(entry.getValue());
List<String> failed = new ArrayList<>();
for (Element rule : elements(rules, "rule")) {
// A rule's context is a pattern; for a plain element name, //name selects what it matches.
NodeList matches = (NodeList) xpath.evaluate("//" + rule.getAttribute("context"), doc, XPathConstants.NODESET);
for (int i = 0; i < matches.getLength(); i++) {
for (Element test : elements(rule, "assert")) {
if (!(Boolean) xpath.evaluate(test.getAttribute("test"), matches.item(i), XPathConstants.BOOLEAN)) {
failed.add(test.getTextContent().strip());
}
}
}
}
System.out.println(entry.getKey() + ": valid against bill.xsd; "
+ (failed.isEmpty() ? "every rule holds" : "fails: " + String.join(" ", failed)));
}
}
static Document parse(String xml) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newNSInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
return dbf.newDocumentBuilder().parse(new InputSource(new StringReader(xml)));
}
static List<Element> elements(Object parent, String localName) {
NodeList list = parent instanceof Document d ? d.getElementsByTagNameNS(SCH, localName)
: ((Element) parent).getElementsByTagNameNS(SCH, localName);
List<Element> out = new ArrayList<>();
for (int i = 0; i < list.getLength(); i++) out.add((Element) list.item(i));
return out;
}
/** The prefixes that the schema's sch:ns elements declare, for the XPath expressions of its rules. */
static NamespaceContext prefixes(Document rules) {
Map<String, String> uris = new LinkedHashMap<>();
for (Element ns : elements(rules, "ns")) uris.put(ns.getAttribute("prefix"), ns.getAttribute("uri"));
return new NamespaceContext() {
public String getNamespaceURI(String prefix) { return uris.getOrDefault(prefix, XMLConstants.NULL_NS_URI); }
public String getPrefix(String uri) { return null; }
public Iterator<String> getPrefixes(String uri) { return List.<String>of().iterator(); }
};
}
} Output
bill.xml: valid against bill.xsd; every rule holds period reversed: valid against bill.xsd; fails: The billing period ends before it starts. 49 units charged as 59: valid against bill.xsd; fails: The units charged differ from the units the meter recorded.
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Rules.java
Rules.java validates each bill against bill.xsd first, then evaluates every assert with the JDK’s XPath 1.0. All three
bills are valid against the schema, and the reversed period and the miscounted units fail one rule each, with the
message the rule carries. The sum of units is exact because units are whole numbers: XPath 1.0 numbers are binary
floating point, so a rule that adds rupees and paise compares them in paise, with round(). The checker reads only
rules whose context is an element name, a small part of the language;
a full Schematron processor, a library, adds phases, reports and variables, and writes its results in SVRL, the report
format that an annex of the standard describes.
RELAX NG: A schema language for XML, standardised as ISO/IEC 19757-2, that describes documents as a grammar of patterns for elements and attributes, in an XML syntax or a compact one, and can use the XML Schema datatypes. describes a document as a grammar of patterns for elements and attributes, in an XML syntax or a compact one, and borrows the XSD datatypes for its values. It checks structure and each value on its own, so rules between values still need Schematron or code, and the JDK has no RELAX NG validator: the last line of Assert11.java’s run asked for one and found none.
Older formats come with a DTD instead, and reading one is enough to understand them. bill.dtd is the bill as an older billing system declared it, without a namespace:
<!-- bill.dtd: the bill as an older billing system described it, without a namespace. -->
<!ELEMENT bill (consumer, period, readings, charges, (due | paid))>
<!ATTLIST bill number ID #REQUIRED>
<!ELEMENT consumer (name, phone?)>
<!ATTLIST consumer number CDATA #REQUIRED>
<!ELEMENT name (#PCDATA)>
<!ELEMENT phone (#PCDATA)>
<!ELEMENT period EMPTY>
<!ATTLIST period from CDATA #REQUIRED
to CDATA #REQUIRED>
<!ELEMENT readings (reading, reading+)>
<!ELEMENT reading (#PCDATA)>
<!ATTLIST reading on CDATA #REQUIRED>
<!ELEMENT charges (charge+)>
<!ELEMENT charge EMPTY>
<!ATTLIST charge slab NMTOKEN #REQUIRED
units CDATA #REQUIRED
amount CDATA #REQUIRED
meter (main | solar) "main">
<!ELEMENT due (#PCDATA)>
<!ELEMENT paid (#PCDATA)> Read it declaration by declaration:
| In bill.dtd | What it says |
|---|---|
(consumer, period, readings, charges, (due | paid)) |
these children in this order, then due or paid |
(name, phone?), (charge+) |
? is optional, + one or more, * any number |
(reading, reading+) |
two or more readings; with only ?, * and +, at most 12 would take ten nested optional groups |
#PCDATA, CDATA |
any text, so no numbers and no dates |
ID, NMTOKEN, (main | solar) "main" |
a unique name, a name-like token, a list of values with a default |
#REQUIRED, #IMPLIED |
the attribute must be present, or may be left out |
EMPTY |
no content at all, only attributes |
A DTD matches names as they are written, prefix included, because it is older than namespaces, and it can declare entities, the feature the safe-parsing lesson switches off. Read a DTD to learn a legacy format; for new work, write its rules again as an XSD and add what the DTD could not say: types, value bounds and keys such as slabId. Three questions about your own format decide most of the choice:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Schema language | Typed values | Rules that compare values | Runs on the JDK alone | Namespaces |
|---|---|---|---|---|
| DTD What you write: ELEMENT and ATTLIST declarations | Poor Every value is text: bill.dtd accepts units of two hundred | Poor No way to compare two values | Good The JDK's parsers can validate against a DTD | Poor Names are matched as written, prefix included |
| XSD 1.0 What you write: an .xsd file of types and elements | Good xs:decimal, xs:date and facets: 10.295 and a 31st day refused | Poor A reversed period and units that do not add up both pass | Good The built-in validator ran every check of this lesson | Good targetNamespace, with prefixes in key selectors |
| XSD 1.1 What you write: XSD 1.0 plus xs:assert and more | Good The same types as XSD 1.0, and more | Good xs:assert compares typed values inside one element | Poor The JDK refused xs:assert and has no XSD 1.1 factory | Good As XSD 1.0, plus xpathDefaultNamespace |
| RELAX NG What you write: a grammar, in XML or compact syntax | Mixed Uses a datatype library, usually the XML Schema types | Poor Checks structure and each value on its own | Poor No RELAX NG validator in the JDK: a library is needed | Good Names are matched with their namespace |
| XSD 1.0 with Schematron What you write: an .xsd file and a .sch file of rules | Good The XSD 1.0 schema checks the types | Good XPath tests: both broken bills reported with a message | Mixed XSD 1.0 and XPath 1.0 are built in; a full Schematron needs a library | Good sch:ns binds each prefix the rules use |
How the answers decide
- “Must values have types, such as decimals with two places or real dates?” Yes, values have types: rules out DTD. A DTD types nothing: from, to and units are all CDATA, so any text passes.
- “Must a rule compare values, such as a period's dates or units that must add up?” Yes, values must agree: rules out DTD, XSD 1.0 and RELAX NG. Only XSD 1.1 assertions and Schematron rules compare one value with another.
- “Must the check run in a Java service with nothing but the JDK?” Yes, the JDK only: rules out XSD 1.1 and RELAX NG and adds a caveat to XSD 1.0 with Schematron. The JDK has no XSD 1.1 or RELAX NG validator. Schematron's XPath 1.0 tests run with javax.xml.xpath, as Rules.java shows, but a full processor is a library.
In short: XSD 1.0 for typed structure that the JDK checks with nothing added, Schematron beside it for rules between values, XSD 1.1 only when every party’s validator supports it, RELAX NG when a readable grammar matters more than built-in support, and a DTD only to read what already exists.
Checkpoint
Check: DTDs, RELAX NG and Schematron
Practise: judge bill values the way a validator does
The exercise turns the facets of this lesson into code: two functions that list the facets a rate or a consumer number breaks, following the XSD rules for white space, digits and patterns wherever Python’s own would differ:
Exercise · Medium · Python
Judge bill values the way an XSD validator does
Write two functions in bill_values.py that judge a value as bill.xsd's simple types do:
rate_problems(text)for theRatetype: anxs:decimalwithminInclusive0,totalDigits4 andfractionDigits2;consumer_problems(text)for theConsumerNumbertype: anxs:stringwith the pattern[A-Z]{2}[0-9]{10}.
Each returns the list of facets the value breaks, [] for a valid value. rate_problems returns ["lexical"] alone when the text is not an xs:decimal at all, and otherwise lists every broken facet in the order minInclusive, totalDigits, fractionDigits. consumer_problems returns ["pattern"] or [].
Follow the XSD rules, not Python's: xs:decimal collapses only spaces, tabs, carriage returns and line feeds, its digits are 0 to 9 only, and its digit facets count the digits of the value, not of the text.
Starter code · bill_values.py
def rate_problems(text):
"""The facets of the Rate type that text breaks, or ["lexical"]; [] when it is valid."""
return []
def consumer_problems(text):
"""["pattern"] when text does not match the ConsumerNumber pattern; [] when it is valid."""
return [] The sample tests · test_bill_values.py
from bill_values import consumer_problems, rate_problems
def test_valid_rates():
"""accepts 4.71, a sign, a leading or trailing point and zeros the value does not need"""
for text in ["4.71", "+4.71", ".5", "5.", "04.710", "99.99", "123.4", "0", "-0.00"]:
assert rate_problems(text) == [], text
def test_xsd_whitespace():
"""collapses spaces, tabs, carriage returns and line feeds only: a no-break space is not one of them"""
assert rate_problems(" 4.71 ") == []
assert rate_problems("\t4.71\r\n") == []
assert rate_problems("\u00a04.71") == ["lexical"]
def test_not_a_decimal():
"""refuses exponents, commas, other scripts' digits and empty text"""
for text in ["1e2", "4,71", "४.७१", "", ".", "4.71x"]:
assert rate_problems(text) == ["lexical"], repr(text)
def test_each_facet():
"""reports minInclusive, totalDigits and fractionDigits on their own"""
assert rate_problems("-1") == ["minInclusive"]
assert rate_problems("123.45") == ["totalDigits"]
assert rate_problems("12345") == ["totalDigits"]
assert rate_problems("10000") == ["totalDigits"]
assert rate_problems("4.715") == ["fractionDigits"]
def test_digits_of_the_value():
"""counts the digits of the value: 0.05 has two, 1000.5 has five"""
assert rate_problems("0.05") == []
assert rate_problems("0.001") == ["fractionDigits"]
assert rate_problems("1000.5") == ["totalDigits"]
assert rate_problems("1000") == []
def test_every_broken_facet():
"""lists every facet a value breaks, in order: 10.295 has five digits and three of them after the point"""
assert rate_problems("10.295") == ["totalDigits", "fractionDigits"]
assert rate_problems("-123.456") == ["minInclusive", "totalDigits", "fractionDigits"]
def test_consumer_numbers():
"""matches the whole value, keeps spaces and accepts only A to Z and 0 to 9, not a full-width zero"""
assert consumer_problems("MH0123456789") == []
for text in ["XMH0123456789", "MH01234567890", "mh0123456789", " MH0123456789", "MH\uff10123456789"]:
assert consumer_problems(text) == ["pattern"], repr(text) A hint
Strip only " \t\r\n", then check the text with re.fullmatch and the character class [0-9], because \d and Decimal() also accept other scripts' digits. For the digit facets, Decimal(text).normalize().as_tuple() gives the digits and the exponent of the value without trailing zeros: a negative exponent is the number of fraction digits.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
The questions below come back to all six ideas, starting with the one you are least sure of, and explain each answer:
Practice that adapts to you
Practise this lesson
12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up, for freshers and up to 5 years: how does xs:key differ from xs:unique?
Both are identity constraints: a selector picks elements, fields read values from each, and no two picked elements may
have equal values, compared as typed values. The difference is presence. xs:unique checks only the elements that
have a value for every field and skips the others, so a slab without an id passes it. xs:key demands a value on every
element the selector picks, so a missing id is an error, and a key is what an xs:keyref normally refers to. Use a key
for an identifier that must exist, such as a slab id that charges point at, and unique for an optional value that must
not repeat, such as an email address that some customers leave out. In both, write the target namespace’s prefix in
the selector, or it selects nothing.
Two harder questions follow: a supplier schema that the JDK will not load, and a keyref that let a broken reference through. Plan your opening move for each, then check it against the first step that each question links to:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Your Java service validates supplier invoices with the JDK's built-in validator. The supplier's new XSD adds xs:assert elements requiring that each invoice total equals the sum of its lines, and the service can no longer load the schema. What are your options, and which do you choose?
-
Mid to senior
Validation against your XSD passed, yet an imported bill has a charge that points at a tariff slab that does not exist. The schema has a keyref for exactly this case. Why did validation not catch it, and how do you make sure it cannot happen again?
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Do XSD facets apply to the text or to the value?
To the value. The type decides whether white space is collapsed or kept, and totalDigits counts the digits of 4.71, not of a spelling such as 04.710.
-
Does an XSD pattern need ^ and $?
No. It must already match the whole value, and ^ and $ are ordinary characters in an XSD regular expression.
-
What do xs:sequence, xs:choice and xs:all decide?
The order and number of children: these children in this order, one of these, or all of them in any order, since xs:all frees the order but not the count.
-
How does xs:key differ from xs:unique?
Both forbid repeated values among the elements a selector picks. xs:key also demands a value on each one and is what xs:keyref usually points at; xs:unique skips elements without a value.
-
Why can an identity constraint silently check nothing?
A selector or field written without the target namespace's prefix picks no elements, so every document passes. Keep a test document with a broken reference that must fail.
-
What do XSD 1.1 assertions add, and does the JDK run them?
XPath tests that compare typed values inside one element. The JDK refuses them, so where only XSD 1.0 runs, write such rules as Schematron-style XPath checks.
-
xs:decimal or xs:double for an amount of money?
xs:decimal: an exact number with digits, one optional point, an optional sign and no exponent. xs:double is binary floating point, for measurements.
-
What can a DTD not express?
Numbers, dates and namespaces: its text is just text, and it ignores namespaces. Read an old DTD to learn the format, then write its rules as XSD.
Key takeaways
- An XSD declares a format’s elements in its target namespace; a validator checks a document in one pass with the parser, and an error handler that keeps every error reports each problem with its line and the rule it breaks.
- Simple types narrow built-in types with facets that apply to values: white space is collapsed or kept by the type,
digits are counted in the value, and a pattern must match the whole value, with
^and$as plain characters. - Sequence, choice and all set the order and the number of children, and the JDK’s codes 2.4.a, 2.4.d, 2.4.f and 2.4.i say which part of the model a document broke.
- Keys and keyrefs check uniqueness and references inside one document, and a selector written without the prefix of the target namespace silently checks nothing.
- XSD 1.1 assertions compare typed values inside one element, but the JDK refuses them; where only XSD 1.0 runs, state such rules as Schematron-style XPath checks.
- A DTD has no numbers or dates and ignores namespaces: read it to learn a legacy format, then write the format’s rules as XSD.
Terms in this lesson
- assertion (XSD 1.1)
- An xs:assert element on an XSD 1.1 type: an XPath 2.0 test over the element and its descendants, with typed values, that must be true for the element to be valid. XSD 1.0 has no assertions.
- complex type
- In XML Schema, the type of an element that has attributes, child elements or both. Its content model, built from sequence, choice and all groups, says which children may appear, in which order and how often.
- facet
- A rule that narrows an XML Schema simple type, such as pattern, enumeration, maxLength, minInclusive, totalDigits or fractionDigits. A value must satisfy every facet of its type and of the types it was derived from.
- identity constraint
- An XML Schema rule on an element, xs:unique, xs:key or xs:keyref, that picks elements with a selector, reads values from each with fields, and checks that the values are unique or point at existing keys.
- RELAX NG
- A schema language for XML, standardised as ISO/IEC 19757-2, that describes documents as a grammar of patterns for elements and attributes, in an XML syntax or a compact one, and can use the XML Schema datatypes.
- Schematron
- A rule language for XML, standardised as ISO/IEC 19757-3: each rule has a context and asserts, and each assert is an XPath test with a message for people, which suits rules that compare values.
- simple type
- In XML Schema, a type for text with no child elements, such as xs:decimal or xs:date, or a type derived from one by facets. Attributes always have simple types; elements may have them too.
References
- XML Schema Part 1: Structures Second Edition (W3C)
- XML Schema Part 2: Datatypes Second Edition (W3C)
- W3C XML Schema Definition Language (XSD) 1.1 Part 1: Structures (W3C)
- XML Path Language (XPath) Version 1.0: node tests (W3C)
- Extensible Markup Language (XML) 1.0 (Fifth Edition): element type and attribute-list declarations (W3C)
- Namespaces in XML 1.0 (Third Edition) (W3C)
- javax.xml.validation.SchemaFactory (Java SE 25) (Oracle)
- javax.xml.validation.Validator (Java SE 25): setErrorHandler (Oracle)
- RELAX NG specifications (OASIS RELAX NG Technical Committee)
- Schematron: ISO/IEC 19757-3 (Schematron.com)
- Saxon 13 product comparison (feature matrix) (Saxonica)
- Apache Xerces2 Java: XML Schema 1.0 and 1.1 (The Apache Software Foundation)
- xmlschema: an implementation of XML Schema for Python (xmlschema project)
- re: regular expression operations (Python 3.14), re.fullmatch and \d (Python Software Foundation)
- decimal: decimal fixed-point and floating-point arithmetic (Python 3.14) (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress