XML and XSD validation

“Is this XML valid?” needs a qualification: valid against which rules, using which schema and language version? XML parsing, DTD validation, XSD assessment, and Python object binding answer different questions. This guide explains those distinctions and what a test-suite validity result tells you.

Well-formed XML is the starting point

An XML parser checks the document’s syntax: legal characters, properly nested tags, quoted attribute values, entity-reference rules, and a single document element. These are well-formedness requirements, defined by XML 1.0 §2.1. A schema cannot make malformed XML well-formed.

For example, <quantity>many</quantity> is well-formed XML. It is invalid against an XSD declaration requiring quantity to contain an xs:integer. <quantity>3</count> is not well-formed, so instance assessment cannot proceed normally at all.

DTD validation checks another set of rules. A document type definition (DTD) declares element content models, attributes, entities, and notations. The XML specification uses “valid” for a document that has a document type declaration and satisfies the associated DTD’s constraints (XML 1.0 §2.8). Merely accepting a DOCTYPE does not mean a parser has performed DTD validation; XML defines both validating and non-validating processors.

XSD assessment checks an XML information set against XML Schema Definition Language rules. XSD supplies namespace-aware declarations, types, datatype facets, content models, and identity constraints. A document can be XSD-valid without having a DTD. If it has both a DTD and an XSD, satisfying one does not imply satisfying the other. pyxsd is an XSD processor, not a DTD validator.

DTDs can affect the information XSD sees

XSD assessment works on information supplied by XML parsing, rather than on the original character sequence alone. The XML Information Set models that information as items and properties: elements, attributes, character data, namespace names, and declaration information. It is an abstract model, not a requirement to use a particular tree API.

Processing DTD declarations can affect this input even without DTD validation:

  • An internal parsed entity’s replacement text can contribute character data or markup.

  • An ATTLIST declaration can supply an omitted attribute’s default value.

  • A declared attribute type can change attribute-value normalization before XSD checks the resulting value.

These effects follow XML’s rules for attribute defaults, attribute-value normalization, and entity processing. They are distinct from XSD defaults and datatype whitespace processing, which apply during schema assessment.

There is also a direct DTD dependency in the XSD datatypes xs:ENTITY and xs:ENTITIES. An ENTITY value must name an unparsed entity declared in the instance document’s DTD; ENTITIES is a nonempty list of such values. Checking only that each token is an NCName checks its lexical form, not this declaration-dependent requirement. An unparsed entity, declared with NDATA, is not a parsed entity reference to expand into XML text. Naming it does not require fetching its resource.

pyxsd’s default ElementTree/Expat backend expands internal parsed entities and can apply ATTLIST defaults. When the compiled schema can validate ENTITY/ENTITIES, pyxsd also lazily collects unparsed-entity declarations from the prolog/internal subset. Complete evidence lets it reject undeclared names, including values supplied by XSD defaults or checked through derived types. An absent DOCTYPE supplies an empty, complete declaration set. Internal parameter entities are expanded; external parameter entities are skipped without retrieval. Unrelated schemas skip this declaration-collection parse.

No built-in profile retrieves external DTD subsets or entities. An external subset or skipped external parameter entity makes declaration evidence incomplete, so a missing name remains lexical-only with one advisory per document when a check needs the missing evidence. Caller-supplied trees without pyxsd source provenance and custom-policy parser results lack core declaration evidence. Non-seekable instance streams passed to top-level pyxsd.parse() also remain lexical-only, even with an explicit schema; compiled Schema.parse() can collect and replay their prolog. Document.revalidate() serializes the tree without its DOCTYPE, so it carries the original parse’s declaration evidence into the reparse instead of recollecting: a declared value stays valid, complete evidence still rejects an undeclared replacement, and evidence the original parse lacked stays lexical-only. These are implementation limits, not alternative validity rules or security protections. See Validation for source-provenance and reread limits.

Parser policy is a separate decision from validity. The protected profile rejects entity declarations and external identifiers; the strict profile rejects every DOCTYPE. Those refusals are not DTD-validation modes or evidence that the refused document violates its XSD. Internal subsets remain untrusted input when permitted. See XML security and untrusted input for the exact policies, limits, and Expat requirements; XSD validation is not a security boundary.

Schema compilation and instance assessment

An XSD schema document is itself XML, but well-formed XML is not necessarily a legal schema. XSD defines constraints both on the XML representation of schema components and on the components themselves. A schema can also draw components from several documents through inclusion and import. The Structures specifications define the component model and schema composition.

It helps to separate two stages:

  1. Schema compilation: read the schema documents, resolve and compose declarations and types, and check schema legality. A type-derivation cycle or an illegal content model is a schema problem, even before any instance is supplied. Checking a schema document against the schema for schema documents alone does not establish all component constraints.

  2. Instance assessment: use the resulting schema components to assess elements and attributes. Missing required children, invalid datatype values, and broken key references are instance problems. A legal schema does not imply that any particular instance satisfies it.

In pyxsd, Schema.compile() also constructs Python binding classes and records recoverable schema findings in Schema.report. Schema.parse() assesses and binds an instance; its Document.report merges schema findings with instance findings. Compilation returning a Schema, or parsing returning a Document, does not by itself establish validity. Check the reports or use require_valid(). Unreadable or malformed input can instead raise PyXSDError when processing cannot proceed. The reporting contract and its known limitations are documented in Validation.

In particular, loading a component and authorizing a reference to its namespace are separate concerns. pyxsd audits XSD-language component QNames against each referencing document’s direct authored imports, effective target namespace, and built-in vocabularies. A component supplied transitively, by a caller mapping, or by an instance hint does not excuse a missing authored import (import-required). Wildcard notQName is intentionally not audited; annotation payloads, foreign markup, QName-valued defaults/fixed values, and XPath expressions are also excluded. A reference that resolves to no namespace is allowed regardless of the document’s authored imports, matching pyxsd’s lenient bare-name lookup, so no-namespace enforcement is incomplete. This audit leaves global component lookup in place; it does not establish complete composition conformance.

Binding determines which Python objects and values an application receives. An invalid document can still yield a best-effort bound tree and a report containing errors. Strict versus lax binding choices control retention of invalid or unrecognized content, not permission to ignore its findings. Namespace handling is a separate policy field; the XSTS harness uses ParseModes.NAMESPACED. See Parse modes and binding. These binding choices are not the XSD wildcard processContents="strict", "lax", and "skip" rules, which control assessment of wildcard-matched content.

XML versions and XSD versions are independent

Two different languages have versions called “1.0” and “1.1”:

Version choice

What it governs

Where it is selected

XML 1.0 or XML 1.1

XML syntax, character rules, and parsing behavior

The document’s XML declaration, such as <?xml version="1.0"?>, and the XML parser’s capabilities

XSD 1.0 or XSD 1.1

Schema vocabulary, component constraints, datatypes, and assessment rules

The schema processor’s version mode

XML 1.1 changes rules such as permitted control-character references and end-of-line normalization; see XML 1.1 §1.3. XML 1.0’s fifth edition also broadened name characters, so “newer name characters” alone is not a reliable way to identify an XML 1.1 document.

An XSD 1.1 schema can be written as an XML 1.0 document and assess XML 1.0 instances. Changing the XML declaration to version="1.1" does not enable XSD 1.1 assertions. Conversely, selecting XSD 1.1 does not give the underlying XML parser XML 1.1 support. XSD 1.1 explicitly addresses its dependencies on XML and Namespaces versions; see Structures, changes to related specifications.

pyxsd defaults to XSD 1.1 and offers xsd_version="1.0" on Schema.compile() and --xsd-version 1.0 on the CLI. Its ElementTree/Expat XML backend does not implement XML 1.1 parsing semantics. That lexer limitation is independent of pyxsd’s XSD version mode. Neither an xs: prefix nor the shared http://www.w3.org/2001/XMLSchema namespace distinguishes XSD 1.0 from 1.1. See Supported features for version-mode behavior and remaining differences.

What changes between XSD 1.0 and 1.1?

XSD 1.1 adds vocabulary and changes some rules for existing constructs. The following examples explain why the same schema or instance can have different expected verdicts under the two versions; they are not a pyxsd support matrix.

  • Relationships between values: XSD 1.1 introduces complex-type xs:assert and simple-type xs:assertion facets, using XPath expressions to express constraints beyond individual datatype bounds. Conditional type assignment with xs:alternative selects a governing type according to conditions, rather than requiring the instance author to supply xsi:type.

  • Extensible content models: XSD 1.1 adds open content and richer wildcard exclusions, and relaxes restrictions on xs:all members. It also changes Unique Particle Attribution: competition between an element particle and a wildcard particle is allowed, with the element taking precedence. Competition between two element particles or two wildcard particles remains constrained. See Structures, content-model changes.

  • Schema evolution and composition: XSD 1.1 adds conditional inclusion through vc:* attributes and an xs:override composition mechanism. Conditional inclusion chooses which declarations to process based on version or capabilities; it is not the same as instance-time conditional type assignment. See Structures §4.2.

  • Datatypes and value semantics: XSD 1.1 adds types such as xs:dateTimeStamp, which requires an explicit timezone, and the xs:yearMonthDuration and xs:dayTimeDuration duration subtypes. It also changes existing lexical spaces: +INF is added for float and double, and year 0000 becomes permitted in the affected date/time types. See Datatypes, changes since 1.0.

Compatibility was a design goal, not a guarantee of identical verdicts for every edge case. Version-specific expected results can therefore be intentional. An implementation’s failure to match its selected version is still a failure, not something corrected by taking the better result from a different version.

Reports, PSVI, and a Boolean verdict

XSD’s post-schema-validation infoset (PSVI) is the input infoset augmented with assessment information, such as governing declarations and types, typed values, and validity properties. Assessment is broader than a single yes/no answer. In particular, the specification’s [validity] property can be valid, invalid, or notKnown; not every information item is necessarily strictly assessed. See XSD 1.0 Structures §2.1 and XSD 1.1 Structures §2.1.

A pyxsd ValidationReport is an application-facing collection of findings with severities, codes, phases, and optional location or constraint evidence. Its has_errors property answers whether the run recorded error-severity findings. It is not a complete PSVI, and the bound Python tree is not a promise to expose every PSVI property or preserve all source information. A report with no errors is evidence from the checks performed under that run’s settings and known implementation limits, not proof that every possible XSD constraint was checked.

The XSTS adapter reduces the relevant phase’s findings to a top-level validity verdict for comparison with the test metadata. That comparison does not check whether binding retained a particular invalid value, whether every descendant has the correct PSVI properties, or whether a diagnostic identifies the right source occurrence. Those require separate tests.

What XSTS establishes

The W3C XML Schema Test Suite (XSTS) contains catalogued schema and instance cases with expected outcomes and version information. pyxsd pins the upstream corpus revision. Its README, “Running the tests” explicitly limits the suite to top-level valid/invalid outcomes, not correct PSVI generation or conformity of error reporting.

For cases with a checkable Boolean expectation, an agreement result means the adapter’s verdict matches that expectation for the selected profile. Schema cases test schema acceptance or rejection; instance cases test assessment using the supplied schema context. Non-Boolean expectations are not forced into yes/no answers. A timeout, adapter limitation, or unexpected exception is not itself a schema-validity verdict.

Agreement is useful regression and interoperability evidence, but it does not establish:

  • W3C certification or complete coverage of XSD features and their interactions;

  • correct or complete PSVI output, diagnostic quality, or API completeness;

  • performance, memory use, security, or suitability for untrusted input;

  • correctness on untested documents, or a universal ranking of schema engines.

The selected corpus, version profile, adapter behavior, and excluded or uncheckable cases define the measurement’s scope. Disagreements need analysis against the specifications and test metadata; another implementation’s answer is useful evidence, not normative authority.

Continue with XSTS methodology and results for case selection, outcome definitions, baseline provenance, current results, and residual failures. For engine-selection tradeoffs, see pyxsd compared with xmlschema and pyxsd compared with libxml2.

Primary specifications

The language definitions are the W3C Recommendations:

The XSD 1.1 change summaries linked above are non-normative reading aids within the specifications; the component and validation rules define the requirements. The pinned XSTS README defines the upstream suite’s stated scope. For pyxsd’s public contracts, continue with Validation, Parse modes and binding, and XML security and untrusted input.