What is an XSD?

An XSD describes the shape of other XML documents: which elements may appear, in what order, how many times, what their text may contain and which attributes they carry. It is itself XML, so the same parser, editor and XPath expressions work on the schema as on the instance it governs.

The specification is in two parts, both W3C Recommendations: Part 1 for structures, Part 2 for datatypes. Version 1.0 Second Edition (28 October 2004) is what almost every tool implements. Version 1.1 (5 April 2012) added the one thing 1.0 cannot express, a constraint spanning two fields, but only two mainstream processors implement it. That split decides which features you can use.

#The schema element, and what is global

A schema document is one xs:schema element in the namespace http://www.w3.org/2001/XMLSchema. The prefix is conventionally xs (older documentation uses xsd) and carries no meaning; only the URI does. Instances use a second namespace, http://www.w3.org/2001/XMLSchema-instance, which supplies exactly four attributes: xsi:type, xsi:nil, xsi:schemaLocation and xsi:noNamespaceSchemaLocation (XSD 1.1 Part 1 section 2.6).

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="note">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="to"   type="xs:string"/>
        <xs:element name="from" type="xs:string"/>
        <xs:element name="body" type="xs:string" minOccurs="0"/>
      </xs:sequence>
      <xs:attribute name="priority" type="xs:byte" default="0"/>
    </xs:complexType>
  </xs:element>
</xs:schema>

<!-- <note priority="2"><to>Alice</to><from>Bob</from></note> -->
A complete schema, and a document that satisfies it

Direct children of xs:schema are global: only those can be the root of an instance or be referenced with ref or type. Anything nested inside a complex type is local, which looks like bookkeeping until you add a target namespace.

#Simple types and complex types

The type system splits on one question: can this thing have children or attributes? A simple type is text and nothing else. Everything else is complex, in one of four shapes: element-only content, mixed content (text and children interleaved, declared with mixed="true"), simple content (text plus attributes), or empty content. An attribute always has a simple type, since it can carry neither children nor attributes of its own.

<xs:element name="price">
  <xs:complexType>
    <xs:simpleContent>
      <xs:extension base="xs:decimal">
        <xs:attribute name="currency" type="xs:string" use="required"/>
      </xs:extension>
    </xs:simpleContent>
  </xs:complexType>
</xs:element>

<!-- Satisfied by <price currency="GBP">19.99</price>.
     type="xs:decimal" alone would reject the attribute. -->
Text plus an attribute needs simpleContent, not a bare xs:decimal

Named types are derived by extension, which appends particles or attributes, or by restriction, which narrows but makes you restate the entire content model.

#Content models and occurrence

CompositorMeaningRestrictions in XSD 1.0
xs:sequenceThese particles, in this orderNone. May repeat and may nest.
xs:choiceExactly one of these particlesNone. With maxOccurs it gives "any of these, any order".
xs:allAll of these, in any orderMust be the whole content model; every particle an element with maxOccurs 1; no groups or nesting; the type cannot be extended.
The three compositors

minOccurs and maxOccurs default to 1 and take any non-negative integer, maxOccurs also accepting unbounded. They apply to xs:sequence and xs:choice as well as to elements, so a repeating group of ordered children is one attribute on the sequence. Attributes are unordered, so no compositor applies to them: use takes optional (the default), required or prohibited, and use="required" with default is an error.

<xs:complexType name="Order">
  <xs:sequence>
    <xs:sequence minOccurs="1" maxOccurs="5">
      <xs:element name="sku"      type="xs:string"/>
      <xs:element name="quantity" type="xs:positiveInteger"/>
    </xs:sequence>
    <xs:choice minOccurs="0">
      <xs:element name="note"    type="xs:string"/>
      <xs:element name="voucher" type="xs:string"/>
    </xs:choice>
  </xs:sequence>

  <xs:attribute name="id"       type="xs:ID"      use="required"/>
  <xs:attribute name="channel"  type="xs:NMTOKEN" default="web"/>
  <xs:attribute name="internal" type="xs:string"  use="prohibited"/>
</xs:complexType>
Occurrence on a group, and the three values of use

Unique Particle Attribution (XSD 1.0 Part 1 section 3.8.6) requires a processor to decide which particle matches an element without looking ahead. A sequence with an optional line followed by a mandatory line violates it, and Xerces reports "cos-nonambig". The fix is to restructure the model.

#Datatypes, restriction and facets

XSD 1.0 Part 2 defines 44 built-in datatypes: 19 primitives including xs:string, xs:decimal, xs:date and xs:QName, plus 25 derived from them such as xs:integer, xs:token, xs:NCName and xs:ID. You narrow one with xs:restriction and facets, of which 1.0 has twelve: length, minLength, maxLength, pattern, enumeration, whiteSpace, minInclusive, minExclusive, maxInclusive, maxExclusive, totalDigits and fractionDigits. XSD 1.1 adds explicitTimezone and assertions.

<xs:simpleType name="Discount">
  <xs:restriction base="xs:decimal">
    <xs:minInclusive value="0"/>
    <xs:maxExclusive value="100"/>
    <xs:fractionDigits value="2"/>
  </xs:restriction>
</xs:simpleType>

<xs:simpleType name="Tags">
  <!-- satisfied by tags="urgent billing" -->
  <xs:list itemType="xs:NCName"/>
</xs:simpleType>
Facets accumulate; xs:list gives whitespace-separated values

The pattern facet uses the flavour of regular expression defined in Part 2 Appendix F, which is neither Perl nor JavaScript. The whole value must match, so there is nothing to anchor, and outside a character class ^ and $ are not metacharacters but literal characters.

Anchored as if it were Perl
<xs:pattern value="^[A-Z]{2}[0-9]{6}$"/>

<!-- Matches the literal string ^AB123456$ and therefore
     rejects AB123456. The schema compiles cleanly, so the
     mistake only surfaces as a validation failure. -->
The match is implicitly whole-value
<xs:pattern value="[A-Z]{2}[0-9]{6}"/>

<!-- Matches AB123456, and nothing longer or shorter. -->

The whiteSpace facet explains a family of confusing results: preserve on xs:string, replace on xs:normalizedString, collapse on xs:token and everything derived from it, where it is fixed. If leading spaces matter in your data, xs:token trims them and reports nothing.

#targetNamespace and elementFormDefault

targetNamespace declares the namespace a schema defines. Every global declaration belongs to it; local declarations do not, unless you say so, and that asymmetry costs more debugging time than any other feature of XSD.

The form of a local element declaration defaults to elementFormDefault on xs:schema, which itself defaults to unqualified. So in a schema with a target namespace and no elementFormDefault, the root is qualified and every element inside it is in no namespace whatever. The instance below is what almost everyone writes, and it is invalid.

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           targetNamespace="urn:acme:order">
  <xs:element name="order">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="line" type="xs:string" maxOccurs="unbounded"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>
Note the absence of elementFormDefault
Rejected: line inherits the default namespace
<order xmlns="urn:acme:order">
  <line>SKU-1</line>
</order>

<!-- Xerces: cvc-complex-type.2.4.a: Invalid content was found
     starting with element '{"urn:acme:order":line}'.
     One of '{line}' is expected.
     libxml2: Element '{urn:acme:order}line': This element is
     not expected. Expected is ( line ). -->
Accepted: line is in no namespace, as declared
<o:order xmlns:o="urn:acme:order">
  <line>SKU-1</line>
</o:order>

<!-- Or keep the instance and add
     elementFormDefault="qualified" to xs:schema. -->

attributeFormDefault works identically, but there unqualified is the right default: a default namespace declaration never applies to an unprefixed attribute (Namespaces in XML 1.0 section 6.2).

#schemaLocation is a hint, and how schemas combine

xsi:schemaLocation is not a binding. XSD 1.1 Part 1 section 2.6.3 describes it and xsi:noNamespaceSchemaLocation as attributes that "can be used in a document to provide hints as to the physical location of schema documents which may be used for assessment". Section 4.3.2 says a processor "should attempt to dereference" those URIs "unless directed otherwise, for example by the invoking application or by command line option".

Should, not must, and explicitly overridable by whoever invokes the validator. The choice of schema belongs to the party asking for validation, not to the author of the document being validated. SQL Server takes the strong reading and ignores both attributes entirely on xml-typed columns.

<order xmlns="urn:acme:order"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="urn:acme:order order.xsd
                           urn:acme:party party.xsd">

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="config.xsd">
Whitespace-separated pairs, or a single URI for unqualified content

Inside a schema, xs:include merges a document with the same targetNamespace or with none, in which case the included components adopt the including namespace: the chameleon include. xs:import declares a dependency on a different namespace, and its schemaLocation is optional because the processor may already hold it. XSD 1.0 also has xs:redefine, badly specified and inconsistently implemented; 1.1 deprecates it for xs:override, which replaces a component outright rather than requiring the replacement to derive from it.

The validator here runs libxml2 compiled to WebAssembly, with XML_PARSE_NO_XXE, XML_PARSE_NONET and XML_PARSE_NO_SYS_CATALOG on every parse and no input provider registered. No code path exists that could fetch anything, so a schemaLocation URL is never dereferenced and a remote xs:include will not resolve: paste the dependency instead.

#XSD 1.0 versus 1.1, and which tools support which

Appendix G of XSD 1.1 Part 1 gives the Working Group three goals: clarity without breaking compatibility, versioning support, and co-occurrence constraints. The last is the reason to care. In 1.0 there is no way to say that one field constrains another, so "the end date must not precede the start date" lives in application code.

  • xs:assert: an XPath 2.0 expression evaluated against the element being validated. The xs:assertion facet does the same for simple types and attributes, where xs:assert is not allowed.
  • xs:alternative: conditional type assignment, the type chosen by an XPath test on the element attributes rather than by the author asserting xsi:type.
  • xs:openContent and xs:defaultOpenContent: undeclared elements interleaved among declared particles or allowed as a suffix, replacing the wildcard plumbing 1.0 needed.
  • Relaxed determinism. UPA is retained, not dropped, but no longer violated by an optional element followed by a wildcard that could also match it, and xs:all particles may repeat.
  • Identity constraints use full XPath 2.0 rather than the restricted subset of 1.0; xs:override replaces xs:redefine; and four datatypes are added, xs:anyAtomicType, xs:dateTimeStamp (timezone required), xs:dayTimeDuration and xs:yearMonthDuration, plus xs:error, which has no valid instances.
<xs:complexType name="DateRange">
  <xs:sequence>
    <xs:element name="start" type="xs:date"/>
    <xs:element name="end"   type="xs:date"/>
  </xs:sequence>
  <xs:assert test="xs:date(start) le xs:date(end)"/>
</xs:complexType>

<xs:element name="payment">
  <xs:alternative test="@kind = 'card'" type="CardPayment"/>
  <xs:alternative test="@kind = 'sepa'" type="SepaPayment"/>
  <xs:alternative type="xs:error"/>
</xs:element>
The constraint 1.0 cannot express, and value-driven typing
Implementation1.01.1Notes
Apache Xerces-JYesYesFully compliant from 2.12.0. Free, and the de facto reference.
Saxon-EEYesYesComplete since 9.5. Enterprise Edition only: HE and PE have no schema processor.
libxml2, xmllintPartialNoIts own API reference titles the module "incomplete XML Schemas structure implementation". What nearly every free validator runs.
.NET System.Xml.SchemaYesNoXmlSchemaSet is 1.0 only; Microsoft has no plans to advance the stack.
Python lxmlPartialNoWraps libxml2, inheriting its engine and gaps.
Python xmlschemaYesYesIndependent, pure Python, not a libxml2 binding.
Where XSD 1.1 is actually available

XSD 1.1 therefore means Xerces-J or Saxon-EE and effectively nothing else: a schema only your build can compile is a schema your trading partner will reject. One correction while you are there. xs:precisionDecimal is not an XSD 1.1 built-in; it was in the 2011 Candidate Recommendation and in many articles written from it, but the final Recommendation dropped it.

import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.File;

// "http://www.w3.org/2001/XMLSchema" would select the 1.0 processor.
// Needs xercesImpl 2.12.0 or later on the classpath.
SchemaFactory factory =
    SchemaFactory.newInstance("http://www.w3.org/XML/XMLSchema/v1.1");

Schema schema = factory.newSchema(new StreamSource(new File("order.xsd")));
Validator validator = schema.newValidator();

// Do not let it fetch anything it was not handed.
validator.setProperty(
    "http://javax.xml.XMLConstants/property/accessExternalSchema", "");

validator.validate(new StreamSource(new File("order.xml")));
Switching Xerces into 1.1 mode: only the constant changes

Common questions

Is XSD the same thing as XML Schema?

Yes, in ordinary use. The W3C Recommendation is called XML Schema, the extension is .xsd, and XSD stands for XML Schema Definition; the 1.1 specification uses XSD in its own title.

Watch the lower-case form. "An XML schema" is sometimes used generically for any schema language, which also covers DTD, RELAX NG and Schematron.

Why does a document that looks correct get rejected?

If the message names an element you can see in the file, it is almost always the elementFormDefault trap above: the expected name prints without a namespace and the found name with one, so they look identical.

After that, the usual causes are a facet failing on whitespace (xs:token and its derivatives collapse it before the value is checked, xs:string does not) and a dateTime with no timezone where the receiver expected one, which XSD 1.0 cannot enforce.

Should I write XSD 1.1?

Only if you control every processor that will see the schema. xs:assert solves a real problem, and moving a cross-field rule out of application code is worth doing where you can.

But Xerces-J and Saxon-EE are the practical universe of 1.1 support. If the schema is published for others to validate against, or consumed by anything on .NET, write 1.0 and put the co-occurrence rules in Schematron: an ISO standard built for exactly those assertions, layered on top of an XSD 1.0 schema, with far more implementations.

Sources

Try it