XML Tutorial · Validation · Lesson 9 of 28
XML Schema (XSD) Basics
Writing an XML Schema with elements, simple and complex types, attributes and occurrence rules, and validating against it.
What XSD adds
W3C XML Schema (XSD, 2001) is written in XML and adds what DTDs lack: data types (xs:integer,
xs:decimal, xs:date, xs:boolean…), namespace support, and reusable types. It's the most widely used schema
language in enterprise integration (SOAP/WSDL, ISO 20022, UBL, HL7).
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="order">
<xs:complexType>
<xs:sequence>
<xs:element name="customer" type="xs:string"/>
<xs:element name="item" maxOccurs="unbounded">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="qty" type="xs:positiveInteger" use="required"/>
<xs:attribute name="price" type="xs:decimal" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="delivery" type="xs:date" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="id" type="xs:integer" use="required"/>
</xs:complexType>
</xs:element>
</xs:schema>
- Simple types hold text (
xs:string,xs:date); complex types have child elements or attributes. xs:sequence(in order),xs:choice(one of),xs:all(any order, each at most once).minOccurs/maxOccurscontrol repetition (both default to 1;unboundedmeans no limit).- Attributes are optional unless
use="required".
Validating in three languages
Each language checks the same three documents against the schema above. Error messages differ between libraries, so the shared output just says valid or invalid:
from lxml import etree
XSD = b"""<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="order">
<xs:complexType>
<xs:sequence>
<xs:element name="customer" type="xs:string"/>
<xs:element name="item" maxOccurs="unbounded">
<xs:complexType><xs:simpleContent><xs:extension base="xs:string">
<xs:attribute name="qty" type="xs:positiveInteger" use="required"/>
<xs:attribute name="price" type="xs:decimal" use="required"/>
</xs:extension></xs:simpleContent></xs:complexType>
</xs:element>
<xs:element name="delivery" type="xs:date" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="id" type="xs:integer" use="required"/>
</xs:complexType>
</xs:element>
</xs:schema>"""
DOCS = {
"good": b'<order id="1"><customer>Ada</customer><item qty="2" price="3.50">Tea</item><delivery>2026-11-02</delivery></order>',
"bad qty": b'<order id="2"><customer>Ada</customer><item qty="0" price="3.50">Tea</item></order>',
"bad date": b'<order id="3"><customer>Ada</customer><item qty="1" price="3.50">Tea</item><delivery>02/11/2026</delivery></order>',
}
schema = etree.XMLSchema(etree.fromstring(XSD))
for name, xml in DOCS.items():
print(name, "->", "valid" if schema.validate(etree.fromstring(xml)) else "invalid")
Output
good -> valid bad qty -> invalid bad date -> invalid
<?php
$xsd = '<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="order">
<xs:complexType>
<xs:sequence>
<xs:element name="customer" type="xs:string"/>
<xs:element name="item" maxOccurs="unbounded">
<xs:complexType><xs:simpleContent><xs:extension base="xs:string">
<xs:attribute name="qty" type="xs:positiveInteger" use="required"/>
<xs:attribute name="price" type="xs:decimal" use="required"/>
</xs:extension></xs:simpleContent></xs:complexType>
</xs:element>
<xs:element name="delivery" type="xs:date" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="id" type="xs:integer" use="required"/>
</xs:complexType>
</xs:element>
</xs:schema>';
$docs = [
"good" => '<order id="1"><customer>Ada</customer><item qty="2" price="3.50">Tea</item><delivery>2026-11-02</delivery></order>',
"bad qty" => '<order id="2"><customer>Ada</customer><item qty="0" price="3.50">Tea</item></order>',
"bad date" => '<order id="3"><customer>Ada</customer><item qty="1" price="3.50">Tea</item><delivery>02/11/2026</delivery></order>',
];
libxml_use_internal_errors(true); // collect errors instead of printing warnings
foreach ($docs as $name => $xml) {
$doc = new DOMDocument();
$doc->loadXML($xml);
echo $name, " -> ", $doc->schemaValidateSource($xsd) ? "valid" : "invalid", "\n";
libxml_clear_errors();
}
Output
good -> valid bad qty -> invalid bad date -> invalid
import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.*;
import java.io.StringReader;
import java.util.LinkedHashMap;
public class Main {
public static void main(String[] args) throws Exception {
String xsd = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="order">
<xs:complexType>
<xs:sequence>
<xs:element name="customer" type="xs:string"/>
<xs:element name="item" maxOccurs="unbounded">
<xs:complexType><xs:simpleContent><xs:extension base="xs:string">
<xs:attribute name="qty" type="xs:positiveInteger" use="required"/>
<xs:attribute name="price" type="xs:decimal" use="required"/>
</xs:extension></xs:simpleContent></xs:complexType>
</xs:element>
<xs:element name="delivery" type="xs:date" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="id" type="xs:integer" use="required"/>
</xs:complexType>
</xs:element>
</xs:schema>""";
var docs = new LinkedHashMap<String, String>();
docs.put("good", "<order id=\"1\"><customer>Ada</customer><item qty=\"2\" price=\"3.50\">Tea</item><delivery>2026-11-02</delivery></order>");
docs.put("bad qty", "<order id=\"2\"><customer>Ada</customer><item qty=\"0\" price=\"3.50\">Tea</item></order>");
docs.put("bad date", "<order id=\"3\"><customer>Ada</customer><item qty=\"1\" price=\"3.50\">Tea</item><delivery>02/11/2026</delivery></order>");
Schema schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
.newSchema(new StreamSource(new StringReader(xsd)));
for (var entry : docs.entrySet()) {
String result;
try {
schema.newValidator().validate(new StreamSource(new StringReader(entry.getValue())));
result = "valid";
} catch (org.xml.sax.SAXException e) {
result = "invalid";
}
System.out.println(entry.getKey() + " -> " + result);
}
}
}
Output
good -> valid bad qty -> invalid bad date -> invalid
Run this on your computer — see “Get Started” for setup.
Reading the errors
Good error messages point at the line and the rule that failed; log them when you reject a message:
from lxml import etree
schema = etree.XMLSchema(etree.fromstring(b"""<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="person">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element name="born" type="xs:date"/>
</xs:sequence>
<xs:attribute name="id" type="xs:integer" use="required"/>
</xs:complexType>
</xs:element>
</xs:schema>"""))
doc = etree.fromstring(b"<person><born>1815-12-10</born><nickname>Ada</nickname></person>")
print(schema.validate(doc))
for err in schema.error_log:
print(f"line {err.line}: {err.message}")
Output
False line 1: Element 'person': The attribute 'id' is required but missing. line 1: Element 'born': This element is not expected. Expected is ( name ).
Test yourself
Which XSD attribute allows an element to repeat any number of times?
maxOccurs="unbounded"minOccurs="0"use="required"type="xs:list"
Show answer
Answer: maxOccurs="unbounded"
maxOccurs sets the upper limit; unbounded removes it.