Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 25 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7, Ruby 4.0.7, sql.js 1.14.2 and Java temurin-25.0.4+101.0.LTS
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

A synthetic electricity bill and its XML Schema

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:

Interactive · stage by stage One pass of the validator over broken.xml Step through the checks in the order the validator meets them, from the parse to the report. Watch which check catches each of the five mistakes.
One pass of the validator over broken.xml, step 9 of 9. Showing Parse the text, Declaration of the root, Check every value (4 problems so far), Check the children (1 problem), Check keys and keyrefs (no problem), Report the problems (5 problems). In focus: Report the problems.Parse the textwell-formed: yesDeclaration of the rootbill in urn:example:billCheck every value4 problems so farCheck the children1 problemCheck keys and keyrefsno problemReport the problems5 problems

The drawing shows the whole scene; its 9 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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:

Four bills through the JDK's XSD validator Java · bill/Validate.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.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

  1. Question 1 of 2 A copy of broken.xml has its end tag </tariff> mistyped at line 14. Which problems does Validate.java report for it?

    Choose one answer.

    A hint

    The fourth document of the run had its mistake at line 18. Count the problems that come before line 14.

    Show the answer: Question 1 of 2

    Answer: The problems at lines 2, 6 and 13, then the fatal error at line 14

    There is one pass. Problems before a syntax error reach the error handler as the parser reads the document; the syntax error is fatal and ends the pass, so nothing after it is checked.

  2. Question 2 of 2 Why did the copy of bill.xml without xmlns="urn:example:bill" get a single error, cvc-elt.1.a, at line 2?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Its root is now bill in no namespace, which bill.xsd does not declare, so nothing below it can be checked

    Every name in a schema with a targetNamespace is an expanded name. A document whose names are in another namespace, or in none, matches no declaration.

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:

Single values against Rate, ConsumerNumber and a pattern with ^ and $ Java · bill/Facets.java
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.710 and 4.71 are valid. xs:decimal collapses white space before anything else, and its digit facets count the digits of the value 4.71, not of the text: the specification defines totalDigits on the value and lets extra leading and trailing zeros through.
  • 123.4 is valid and 123.45 is not. Four digits in all are allowed and five are not, wherever the point falls.
  • 10.295 fails fractionDigits, 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:

The ConsumerNumber pattern with re.search and re.fullmatch Python · patterns/anchored.py
"""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

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

  1. Question 1 of 2 The Rate type is an xs:decimal with minInclusive 0, totalDigits 4 and fractionDigits 2. Which values does it accept? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • 123.4
    • 4.71 , with a space on each side
    • 04.710

    Facets apply to the value, not the text: totalDigits and fractionDigits count the digits of 4.71 in 04.710, and white space is collapsed before anything is checked.

  2. Question 2 of 2 A schema gives a PIN code the pattern ^[0-9]{6}$. Which value does it accept?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: ^560001$, with the two symbols as part of the value

    XSD patterns are anchored at both ends already, so ^ and $ add nothing but two literal characters. Write [0-9]{6}.

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:

Interactive · stage by stage The children of bill against the Bill sequence Step through the children of the bill one at a time. Watch which part of the sequence each one fills, and where one finds no place.
The children of bill against the Bill sequence, step 5 of 5. Showing consumer, period, readings, tariff, charges, due or paid (due: no place left). In focus: due or paid.consumeronce: name, phone?periodoncereadings2 to 12 readingtariff1 or more slabcharges1 or more chargedue or paiddue: no place left

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. period and readings. period fills the second place, and readings the third. readings holds two reading elements, inside its range of 2 to 12.
  3. 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.
  4. The choice. paid takes the last place in the sequence, the choice of due or paid. With it, the content of bill is complete.
  5. 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:

Four mistakes of order and number Java · bill/Order.java
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

  1. Question 1 of 2 Consumer is an xs:all of name and an optional phone. Which contents of consumer are valid? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • name alone
    • phone, then name

    xs:all frees the order, not the count. XSD 1.1 relaxes the count, and allows maxOccurs above 1 in an all group.

  2. Question 2 of 2 How many reading elements may readings hold at most, as bill.xsd declares them?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 12 reading elements

    The particle has minOccurs="2" and maxOccurs="12"; Order.java's copy with thirteen readings failed with cvc-complex-type.2.4.f at line 20.

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:

Interactive · step by step A key on the slabs, a keyref from the charges Step through the check of the bill whose slab s3 was renamed s2, then the same check with selectors written without b:.
A key on the slabs, a keyref from the charges, step 6 of 6. Showing slab s1 (not selected), slab s2 (not selected), slab s2 (not selected), charge for s1 (not selected), charge for s2 (not selected), charge for s3 (not selected).slab s1not selectedslab s2not selectedslab s2not selectedcharge for s1not selectedcharge for s2not selectedcharge for s3not selected

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. Collect the references. The selector of chargeSlab, b:charges/b:charge, picks the three charges, and @slab reads s1, s2 and s3 from them.
  4. Two references resolve. s1 and s2 are values of the key, so the first two charges point at slabs that exist.
  5. 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.
  6. 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:

A duplicate key, a missing reference, and selectors without a prefix Java · bill/Keys.java
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

  1. Question 1 of 2 A keyref's selector is written charges/charge in a schema whose targetNamespace is urn:example:bill. A charge points at a missing slab. What does validation report?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing: the selector matches no element, so the keyref checks nothing

    Write the prefix the schema binds to its target namespace into every step of a selector and a field, and keep a test document with a broken reference that must fail.

  2. Question 2 of 2 What does xs:key require that xs:unique does not?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Every element the selector picks must have a value for each field

    A key is unique and present everywhere it is selected; a unique constraint only forbids duplicates among the values that are there.

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:

Two XSD 1.1 assertions, offered to the JDK Java · bill/Assert11.java
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:

Timeline · 6 events in order DTDs, XSD 1.0 and 1.1, RELAX NG and Schematron Walk through the schema languages in the order they appeared. The tags say what each one checks best.
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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

  1. Question 1 of 2 You move the units rule into the type of charges, as xs:assert test="sum(b:charge/@units) eq ../b:readings/b:reading[last()] - ../b:readings/b:reading[1]". What will an XSD 1.1 validator do with it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Find the assertion false for every bill, because charges has no parent in the tree the test sees

    Put an assertion on the type of the smallest element that contains every value it compares: here, Bill.

  2. Question 2 of 2 What did the JDK do with bill.xsd once two xs:assert elements were added to the Bill type?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It refused the schema: element assert is invalid, misplaced, or occurs too often

    Assert11.java recorded both answers: the 1.0 factory refused the schema, and no factory implements XSD 1.1.

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:

The two rules as ISO Schematron, checked with the JDK's XPath

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, '-', '') &lt;= 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 system declared it DTD · legacy/bill.dtd
<!-- 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:

Interactive · answer and compare Which schema language fits your format? Answer three questions about your documents and the program that checks them; each answer rules options out or adds a caveat.
Must values have types, such as decimals with two places or real dates?
Must a rule compare values, such as a period's dates or units that must add up?
Must the check run in a Java service with nothing but the JDK?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Five ways to state the rules of an XML format, against four things that decide the choice
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

  1. Question 1 of 2 Which of these does bill.dtd accept? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • A charge with units="two hundred"
    • Thirteen reading elements

    A DTD checks names, order and counts, and a few attribute kinds, but has no numbers or dates: from, to and units are all text to it.

  2. Question 2 of 2 A Java service with no libraries beyond the JDK must check bill types and the rule that units charged equal units read. What do you use?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: XSD 1.0 for structure and types, then the rule as an XPath 1.0 check, Schematron style

    Split the checks: the schema for what each value is, the rules for how values relate.

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 the Rate type: an xs:decimal with minInclusive 0, totalDigits 4 and fractionDigits 2;
  • consumer_problems(text) for the ConsumerNumber type: an xs:string with 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.

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.

  1. Question 1 of 12 Keys.java reported the repeated slab id s2 at line 14, where the second s2 stands, but the missing slab s3 at line 22, the end tag of bill. Why so late?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: A reference can be judged only once every key value of bill is known, and a key could still come later

    Identity constraints belong to the element that declares them, bill here. Key values are collected while the slabs are read, and a repeated one is reported at its own line; each keyref value is looked up when bill ends.

  2. Question 2 of 12 A teammate's validation code calls validate() with no error handler. On broken.xml, what will it report?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: The first problem only, at line 2, as an exception

    Set an error handler that records every SAXParseException, as Validate.java does, so one run lists every problem with its line.

  3. Question 3 of 12 Does 0.05 satisfy totalDigits 1?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: No: as i × 10^-n it is 5 × 10^-2, and n = 2 is more than totalDigits allows

    XML Schema defines totalDigits on the value: numbers of the form i × 10^-n with |i| below 10^totalDigits and n at most totalDigits.

  4. Question 4 of 12 You test an XSD pattern in Python before putting it in a schema. Which call answers the way the schema will?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: re.fullmatch(pattern, value)

    Use fullmatch, and remember that the dialects differ in other ways: XSD has no ^ or $ anchors, and its \i and \c classes do not exist in Python.

  5. Question 5 of 12 The JDK reports: Invalid content was found starting with element charges. One of tariff is expected. What is wrong?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: charges came where the sequence still expects tariff

    2.4.a names the element that arrived and the elements that could have come instead: read the second half first.

  6. Question 6 of 12 A new bill version may hold any number of notes and adjustments, mixed, after the charges. Which XSD 1.0 content model fits?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: A choice of note and adjustment with maxOccurs="unbounded"

    A repeating choice frees both the order and the count; XSD 1.1 also lets an all group repeat its elements.

  7. Question 7 of 12 What did the JDK report when two slabs had the id s2?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: cvc-identity-constraint.4.2.2: a duplicate key value for slabId

    The message names the rule's clause, the value and the constraint: Duplicate key value [s2] declared for identity constraint slabId.

  8. Question 8 of 12 How do you make sure a keyref still works after someone edits the schema?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: Keep a test document with a reference to a missing key, and require validation to fail on it

    Keys.java's third run is the failure to test for: valid, with a reference that points at nothing.

  9. Question 9 of 12 In the assertion b:period/@from le b:period/@to, how are the two attributes compared?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: As xs:date values, because assertions see the typed values of the descendants

    XSD 1.1 types the elements and attributes below the element being validated, so comparisons follow their declared types.

  10. Question 10 of 12 Which feature belongs to XSD 1.1 and not to XSD 1.0?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: xs:assert

    XSD 1.1 adds assertions, conditional type assignment, open content and freer all groups to everything XSD 1.0 had.

  11. Question 11 of 12 What does a Schematron assert do when its test is false?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: It reports the message written inside the assert

    Each assert carries a message for people, which is why Schematron suits business rules.

  12. Question 12 of 12 A legacy DTD declares b:bill. A partner sends the same document with the prefix x: bound to the same namespace. What happens under DTD validation?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: It fails: a DTD matches element names as written, prefix included

    One of the reasons new formats use XSD or RELAX NG: both match names by namespace, whatever the prefix.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. Does an XSD pattern need ^ and $?

    No. It must already match the whole value, and ^ and $ are ordinary characters in an XSD regular expression.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.