A small, fast and dependency-free XML library for Delphi 6 through Delphi 13 and Free Pascal 3.2+: a pull reader, a streaming writer, a DOM and locale-independent conversion helpers for XML Schema values.
Status: version 0.4.0. The API may still change in details before 1.0; see the CHANGELOG.
- No dependencies: only
SysUtils,ClassesandContnrs. One code base for Ansi Delphi (6 to 2007), Unicode Delphi (2009 to 13, Win32 and Win64) and Free Pascal (Windows, Linux, macOS). - XML 1.0 with namespaces: well-formedness is checked strictly, errors carry an error code, line, column and byte offset.
- Input encodings: UTF-8, UTF-16 LE/BE, ISO-8859-1, ISO-8859-15, Windows-1252 and US-ASCII; everything is converted to UTF-8 internally.
- Pull reader (
TLandrixXmlReader) for large files, with zero-copy access to names and values. - Streaming writer (
TLandrixXmlWriter) with correct escaping, namespace checks, optional indentation that never alters mixed content, and base64 output for attachments. - DOM (
TLandrixXmlDocument) with search by namespace and local name, mixed content (GAEB long texts), line and column per node, and saving through the writer. - Conversion helpers for
xs:decimal(Currency,Double), integers,xs:boolean,xs:date,xs:dateTime,xs:time, the CII date formats 102 and 610, and base64 – all independent of locale settings. - Safe by default: no DTD processing, no expansion of entities declared in a DTD (the five predefined entities and character references are supported), no external resources, no network access, nesting depth limit (256 by default).
| Unit | Contents |
|---|---|
LandrixXmlBase |
string types, UTF-8/UTF-16 conversion, ELandrixXmlError, byte buffer, base64 |
LandrixXmlEncoding |
input encoding detection and conversion (used by the reader) |
LandrixXmlReader |
pull reader TLandrixXmlReader, push adapter TLandrixXmlHandler |
LandrixXmlWriter |
streaming writer TLandrixXmlWriter |
LandrixXmlDom |
TLandrixXmlDocument, TLandrixXmlNode |
LandrixXmlConvert |
xs:* value conversion |
LandrixXmlDecimal |
exact xs:decimal/xs:integer arithmetic with Saxon-compatible division (first of the optional validation units; the units above do not use it) |
LandrixXmlDouble |
xs:double and xs:float in software (IEEE 754 bit pattern, Saxon-compatible string value), optional validation unit |
LandrixXmlRegex |
XSD and XPath regular expressions, linear-time matching, optional validation unit |
LandrixXmlUnicode |
Unicode 6.2.0 general categories, code point strings, optional validation unit |
LandrixXmlNormalize |
Unicode normalization NFC, NFD, NFKC, NFKD and the exact check whether a text is normalized (Unicode 15.0.0), optional validation unit |
LandrixXmlXsdTypes |
XML Schema simple types and facets, optional validation unit |
LandrixXmlXsdContent |
XML Schema content models (sequence, choice, occurrence bounds, wildcards), optional validation unit |
LandrixXmlXsdSchema |
loaded XML Schema components, optional validation unit |
LandrixXmlXsdSchemaLoader |
loads XML Schema documents (include, import, redefine), optional validation unit |
LandrixXmlXsdValidator |
validates documents (reader or DOM) against a loaded schema, optional validation unit |
LandrixXmlXPathTree |
read-only document tree for XPath (from reader or DOM), optional validation unit |
LandrixXmlXPath |
XPath 2.0 parser and static context, optional validation unit |
LandrixXmlXPathEval |
evaluates XPath 2.0 expressions on the tree, as XPath 2.0 specifies or as Saxon HE runs them (two profiles), optional validation unit |
LandrixXmlSchematron |
Schematron validation (EN16931, XRechnung, Peppol BIS, Factur-X), SVRL output, optional validation unit |
LandrixXmlSchematronCompiler |
compiles ISO Schematron (.sch) into rule files, optional validation unit |
LandrixXmlSha256 |
SHA-256, used for the source fingerprint of compiled rules |
Add the source folder to the search path (Delphi: unit and include path;
Free Pascal: -Fusource -Fisource).
The examples below are taken from examples/LandrixXmlExamples.dpr, which is compiled and run with every supported compiler.
Writer := TLandrixXmlWriter.Create; { or CreateFile('invoice.xml') }
try
Writer.Indent := True;
Writer.StartElementNs(NsInvoice, '', 'Invoice');
Writer.DeclareNamespace('cac', NsCac);
Writer.DeclareNamespace('cbc', NsCbc);
Writer.ElementUtf8('cbc:ID', 'R-2026-0042');
Writer.ElementUtf8('cbc:IssueDate', LandrixXmlFormatDate(EncodeDate(2026, 10, 15)));
Writer.StartElement('cac:LegalMonetaryTotal');
Writer.StartElement('cbc:PayableAmount');
Writer.AttributeUtf8('currencyID', 'EUR');
Writer.TextUtf8(LandrixXmlFormatCurrency(119, 2));
Writer.EndElement;
Writer.EndElement;
Writer.EndElement;
Writer.EndDocument;
Result := Writer.AsUtf8;
finally
Writer.Free;
end;The XML declaration <?xml version="1.0" encoding="UTF-8"?> is written
automatically. A call that would produce invalid XML (an undeclared
prefix, a control character, -- in a comment, ...) raises
ELandrixXmlError and leaves the output unchanged.
Reader := TLandrixXmlReader.Create;
try
Reader.LoadFromFile('invoice.xml');
while Reader.Read do
case Reader.TokenType of
xttStartElement:
if Reader.NamespaceUriUtf8 = NsCbc then
begin
if Reader.LocalNameUtf8 = 'ID' then
Number := Reader.ReadElementText { the text inside }
else if Reader.LocalNameUtf8 = 'IssueDate' then
Issued := LandrixXmlParseDate(Reader.ReadElementTextUtf8)
else if InTotal and (Reader.LocalNameUtf8 = 'PayableAmount') then
Amount := LandrixXmlParseCurrency(Reader.ReadElementTextUtf8);
end
else if (Reader.NamespaceUriUtf8 = NsCac) and
(Reader.LocalNameUtf8 = 'LegalMonetaryTotal') then
InTotal := True;
xttEndElement:
if Reader.LocalNameUtf8 = 'LegalMonetaryTotal' then
InTotal := False;
else
{ text, comments, ...: nothing to do }
end;
finally
Reader.Free;
end;Elements are matched by namespace URI and local name, so the prefixes
used in the document do not matter. ReadElementText reads the text of a
simple element up to its end tag: text and CDATA are joined, comments and PIs
are ignored, and a child element raises xecUnexpectedElement (the reader stays
on the child, so it can be skipped). Reader.Skip skips a whole subtree.
Reader.ElementPath gives the path of the current element, such as
/Invoice/cac:InvoiceLine/cbc:ID, for messages about skipped elements;
after an ELandrixXmlError it still tells in which element the error
occurred.
Doc := TLandrixXmlDocument.Create;
try
Doc.LoadFromFile('bill.x86');
Body := Doc.Root.FindChild(NsGaeb, 'Award').FindChild(NsGaeb, 'BoQ')
.FindChild(NsGaeb, 'BoQBody');
Item := Body.FirstChildElement;
while Item <> nil do
begin
Qty := LandrixXmlParseDouble(Item.ChildTextUtf8(NsGaeb, 'Qty'));
Text := Item.FindDescendant(NsGaeb, 'Text');
LongText := Text.Text; { all text of the paragraphs and spans }
Item := Item.NextSiblingElement;
end;
finally
Doc.Free;
end;Text, elements, CDATA sections, comments and processing instructions are
separate child nodes, so formatted long texts (<p>, <span>, tables)
are kept exactly. FindChildLocal('Item') matches the local name in any
namespace. Missing children give nil, missing attributes and child texts
an empty string.
Doc := TLandrixXmlDocument.Create;
try
Doc.Indent := True;
Doc.LineBreak := xlbCRLF;
Root := Doc.AddElement(NsGaeb, '', 'GAEB');
Item := Root.AddElement('Award').AddElement('Item');
Item.SetAttribute('ID', '1');
Item.AddElement('Qty').TextUtf8 := LandrixXmlFormatDouble(12.5, 3);
Doc.SaveToFile('bill.x86');
finally
Doc.Free;
end;AddElement('prefix:name') resolves the prefix through the declarations in
scope; AddElement(Uri, Prefix, LocalName) declares it where needed.
Schema := LandrixXmlXsdLoadSchema('order.xsd'); { include, import, redefine }
try
Validator := TLandrixXmlXsdValidator.Create(Schema);
try
if not Validator.ValidateFile('order.xml') then
for I := 0 to Validator.ErrorCount - 1 do
WriteLn(Validator.Errors[I].Line, ':', Validator.Errors[I].Column, ' ',
Validator.Errors[I].Code, ' ', Validator.Errors[I].Message);
{ 3:26 cvc-datatype-valid.1.2.1 '2026-02-30' is not a valid value for 'date'. }
finally
Validator.Free;
end;
finally
Schema.Free;
end;The errors carry the codes and messages of Xerces, the validator of the
Java runtime behind the KoSIT and Mustang validators, with the position
(ValidateFile and ValidateReader: where Xerces reports it, with the
column counted in code points where Xerces counts UTF-16 units, so it
differs after a character outside the BMP on the same line;
ValidateDocument: the start of the element), a path such as
/Order/Item[1]/Qty[1] and the declaration concerned. A schema that
breaks an XML Schema rule (or a document that is no schema) raises
ELandrixXmlXsdSchemaError with ConstraintCode and the schema file and
position; a schema document that is not well-formed raises the reader's
ELandrixXmlError, and a construct the loader does not support
ELandrixXmlError with xecUnsupported (it is never ignored);
ELandrixXmlError is the base class of all of them. A loaded schema set is read-only and can be
shared by validators in several threads.
Rules := TLandrixXmlSchematronRules.Create;
try
Rules.LoadFromSchematronFile('EN16931-UBL-validation.sch');
Validator := TLandrixXmlSchematronValidator.Create(Rules);
try
Valid := Validator.ValidateFile('invoice.xml');
for I := 0 to Validator.ResultCount - 1 do
WriteLn(Validator.Results[I].Id, ' ', Validator.Results[I].Flag, ' ',
Validator.Results[I].Location, ': ', Validator.Results[I].Message);
Validator.SaveSvrlToFile('invoice.svrl'); { optional }
finally
Validator.Free;
end;
finally
Rules.Free;
end;ISO Schematron with the xslt2 query binding is compiled when it is
loaded (LoadFromSchematronFile); tools/sch-compile.py or
TLandrixXmlSchematronCompiler can also compile it once into a rule file
for LoadFromFile. A validation ends in one of four ways:
| Outcome | How it shows |
|---|---|
| valid | ValidateFile returns True. Results may still hold failed asserts and reports flagged warning or information. |
| invalid | False, Incomplete = False: at least one failed assert or successful report with another flag (fatal, error, none). |
| incomplete | False, Incomplete = True: an expression raised a dynamic error (an invalid cast, a type error), shown as a result of kind xsrDynamicError with ErrorCode. The other rules were still checked. Saxon, and so the KoSIT validator, stops with no result here; AbortOnDynamicError := True stops at the first error too. |
| not checked | the document is not well-formed: ValidateFile raises ELandrixXmlError as the DOM does, and there are no results. |
A Schematron file that does not compile raises ELandrixXmlError:
xecSchema for invalid Schematron, xecUnsupported for a construct the
compiler does not implement (it is never ignored), an XPath error for an
expression, each with file and line; the reader's errors for malformed
XML; a file that cannot be opened raises the RTL's stream error. A loaded
TLandrixXmlSchematronRules is read-only and can be shared by validators
in several threads; each validator belongs to one thread.
Tree := TLandrixXmlXPathTree.Create; { read-only, from a file, reader or DOM }
Context := TLandrixXmlXPathStaticContext.Create;
try
Tree.LoadFromFile('order.xml');
Context.AddNamespace('o', 'urn:example:order');
Expr := TLandrixXmlXPathExpression.Create('sum(/o:Order/o:Item/o:Qty)', Context);
Evaluator := TLandrixXmlXPathEvaluator.Create(Tree);
try
WriteLn(Evaluator.EvaluateString(Expr, 0)); { node 0: the document node }
finally
Evaluator.Free;
Expr.Free;
end;
finally
Context.Free;
Tree.Free;
end;Evaluate returns the items (nodes and atomic values) as a
TLandrixXmlXPathResult; EvaluateBoolean and EvaluateSequence are
the other forms. An expression is compiled once and can be evaluated
any number of times; errors raise ELandrixXmlXPathError with the XPath
error code.
try
Doc.LoadFromString('<Invoice><ID>1</Invoice>');
except
on E: ELandrixXmlError do
{ E.Code = xecSyntax, E.Line = 1, E.Column = 17 (the wrong end tag name) }
end;Every error raises ELandrixXmlError with Code (TLandrixXmlErrorCode),
Line and Column (1-based, 0 = unknown) and Offset (0-based byte offset
in the original file, -1 = unknown). Messages are English diagnostics; use
Code to react programmatically. A failed load leaves the document empty.
All text is stored internally as UTF-8. Text is available in three forms:
| Accessor | Type | Lossless |
|---|---|---|
...Utf8 |
TLandrixXmlUtf8 (AnsiString on Delphi 6-2007, UTF8String elsewhere) |
yes |
...W |
TLandrixXmlWide (WideString on Delphi 6-2007, UnicodeString elsewhere) |
yes |
| no suffix | native string |
Delphi 2009+: yes. Delphi 6-2007: converted to the system code page, characters outside it become ?. Free Pascal: passed through when the code page is UTF-8, otherwise converted by the RTL (on Unix add the cwstring unit to your program). |
Names, namespace URIs and other parameters are passed as
TLandrixXmlUtf8; string literals work everywhere. Values passed to the
writer and the DOM come in the same three forms (TextUtf8, TextW,
Text).
Delphi 6 to 2007: use the ...W accessors (or ...Utf8) for text
that may contain characters outside the system code page, e.g. Polish or
Czech names on a German Windows. The native string accessors are
convenient but lossy there.
Invalid Unicode (malformed UTF-8, unpaired UTF-16 surrogates) is never
replaced silently; it raises ELandrixXmlError.
LandrixXmlConvert converts between XML Schema lexical forms and Pascal
values without looking at DecimalSeparator, date formats or any other
locale setting. Every type has LandrixXmlFormatX, LandrixXmlParseX
(raises ELandrixXmlError with xecConversion) and LandrixXmlTryParseX,
and the parsers also take a pointer and a length (...Buf).
xs:decimalforCurrencyis exact:'0.12345'is rejected unless rounding is requested (AllowRounding = True). Rounding is half away from zero;LandrixXmlFormatCurrency(1.005, 2)gives1.01.Doublevalues are parsed correctly rounded for up to 15 significant digits and formatted as written (2.675with two decimals gives2.68).- Dates and times keep their time zone in
TLandrixXmlDateTime; theTDateTimevariants return the value as written, without converting to UTC.
- No DTD processing. A
<!DOCTYPE>is skipped and reported, but not checked; entities declared in it are not expanded, and references to them are errors. No external entities or DTDs are ever read. - Encodings other than the ones listed above are rejected with a clear error.
- XML 1.1 documents are read with XML 1.0 rules.
- Nesting depth is limited to 256 by default (
MaxDepth). - Saving keeps the content, not the bytes: a document saved from the DOM reads back with the same nodes, but quotes, character references and line ends may be written differently.
- White-space dropping (
WhitespaceMode := xwmDropBetweenElements) is lossy by design: in<p><span>A</span> <span>B</span></p>the space between the spans is dropped. The default keeps everything. In indented files it removes about half of the nodes and a quarter of the memory.
For the constructs it supports (the others are rejected as unsupported,
see the W3C XML Schema Test Suite below), the XML Schema validator aims at
the results of Xerces in the Java runtime, the validator behind the KoSIT
and Mustang validators: the same verdicts, error codes, messages and
order. Where Xerces departs from XML Schema 1.0 or from the expectations
of the W3C XML Schema Test Suite, LandrixXml follows Xerces on purpose,
so that a document gets the same verdict as with the official
validators. A profile that follows the specification instead may be
added as an option later; there is none yet. The known cases (with the
test suite's names; in the source code each place is marked with a
comment Xerces deviation: and these names, and every new case gets
both the mark and an entry here):
- Name characters of
Name,NCName,ID,IDREFandNMTOKENare those of XML 1.0 Fourth Edition, as in Xerces; the suite's XML 1.1 / Fifth Edition tests expect the newer ones (xv001,xv002,xv004,xv009). gMonthalso accepts the form--MM--of XML Schema 1.0 before its errata, also in facet values (gMonth_pattern001_1274,gMonth_enumeration002_203,gMonth_maxInclusive002_205,gMonth_maxExclusive002_208,gMonth_minInclusive002_210,gMonth_minExclusive002_213,gMonth_enumeration001_1275,gMonth_enumeration002_1276,gMonth_enumeration003_1277,gMonth_enumeration004_1278,gMonth_maxInclusive001_1279,gMonth_maxInclusive002_1280,gMonth_maxInclusive003_1281,gMonth_maxExclusive001_1282,gMonth_maxExclusive002_1283,gMonth_maxExclusive003_1284,gMonth_minInclusive001_1285,gMonth_minInclusive002_1286,gMonth_minInclusive003_1287,gMonth_minInclusive004_1288,gMonth_minInclusive005_1289,gMonth_minExclusive001_1290,gMonth_minExclusive002_1291,gMonth_minExclusive003_1292,gMonth_minExclusive004_1293,gMonth_minExclusive005_1294).- An exclusive bound only has to be a valid value of the base type, so
maxExclusive="1"onpositiveIntegeris accepted though no value is left (positiveInteger_maxExclusive001_1834,positiveInteger_maxExclusive002_1835). - An
xs:importof a namespace that a schema document loaded or being loaded already has, or that the same document imported before, is skipped, whatever itsschemaLocation, as in Xerces without honour-all-schemaLocations (the setting of the KoSIT validator); the suite expects the components of every location (schG3,schG12). - An
xs:importwithnamespace=""is accepted (schZ014_a,schZ014_b). - An
IDREF/IDREFSattribute value supplied by a default or fixed value of the schema is not checked as a reference, and a key field that selects an attribute matched by a lax or skip wildcard is accepted (idZ012,idZ015). - A field
@*matches the first attribute of an element only, so an element with two attributes (one of themxsi:nil) gives one value (idL100). - An element whose content model is an empty
xs:choicemay be empty (complex022). - A prohibited attribute use with a fixed value (
use="prohibited" fixed="37") does not allow the attribute, unless an attribute wildcard does; the suite expects the attribute with that value to be valid under XML Schema 1.0 (attP031). - The regular expression forms
\p{IsHighSurrogates},\p{IsLowSurrogates}and[a-z--[b-z]]are errors (reF56,reM77,reN75,HighSurrogates). - A missing type (also the item type of a list) or substitution group
head is an error when the schema is loaded, also if nothing uses it;
the suite expects the error only when the component is needed
(
missing001,missing002,missing003,missing006). - The XSD 1.1 version attributes (
vc:minVersionand the like) are not evaluated, so XSD 1.1 constructs they would exclude are errors (vc001,vc002,vc024). - The XSD 1.1 wildcard attributes
notNamespaceandnotQNameare errors (s3_10_1ii08,s3_10_1ii09,s3_10_6ii01,s3_10_6ii02,s3_10_6ii04). - A default or fixed value is not allowed on an element whose type is a
union with an
IDmember, as forIDitself (stE072); a complex type withIDcontent does not count (Xerces never marks one). - Unique Particle Attribution is checked on the content model as Xerces
rewrites it: where its validator counts the bounds of an element, the
element stands for any number of repetitions, so for example a sequence
of
a(exactly twice),aand a repeated choice is ambiguous, though XML Schema 1.0 allows it. - The
finalof a simple type also forbids the extension by a complex type with simple content when it is#allor comes from afinalDefaultthat namesextension; XML Schema 1.0 limits the{final}of simple types to restriction, list and union (no test of the suite covers this; in the source code the mark readsXerces deviation, not in the test suite). - A component that two documents redefine (two
xs:redefineof the same document) is an error (sch-props-correct.2), as Xerces sees two components of the same name; the suite expects the schema to load (schU3,schU4,schU5). - The particles of a restriction are checked as Xerces checks them, so
some restrictions the suite expects to load are rejected: a sequence
whose only element has
maxOccurs="0"stays an empty sequence, which cannot restrict an element (mgE014); a wildcard in a group that restricts a wildcard must fit the occurrence range of that wildcard (particlesHa080,particlesHa084); and an element repeated without bound does not restrict a repeated choice of elements that occur once, which only XML Schema 1.1 allows (particlesZ001). - Empty content may restrict a complex type with simple content, as
Xerces compares only the particles; XML Schema 1.0 requires the base
to have empty or emptiable element content (
particlesZ039). - A union that is a member type of another union stands for its own
member types, so the facets of a restricted union are dropped there
(no test of the suite covers this; in the source code the mark reads
Xerces deviation, not in the test suite). - The prefix
xmlnsis bound in QName values, asxmlis, soxmlns:xsiis a validxs:QName(QName009_2092). - The versioning attributes of XML Schema 1.1 (
vc:typeAvailableand the like) are not read, so a reference to a type that only XML Schema 1.1 has (xs:error) is an error (vc014). - A restriction that has or inherits a
lengthmay giveminLengthormaxLengthonly if its base type has the same one; the errata of XML Schema 1.0 allow more (errF001). xs:durationfollows Xerces: the numbers of years, months, days, hours and minutes may not exceed 2147483647, seconds may start with the point (PT.5S), and the order of durations is computed with Xerces' arithmetic, whose int sums wrap around and whose quotients are floats, so very large durations compare wrongly (62914619 minutes count as more than 1048576 hours and 59 minutes, the same duration) (no test of the suite covers these; in the source code the mark readsXerces deviation, not in the test suite).- Fixed values in a schema (an attribute use against its declaration, a
restriction against its base) compare as Java's equals of Xerces'
values, so date and time values of different types are equal if their
fields are, with Xerces' placeholders for the missing ones (year 2000,
month 1, day 1, or 15 for
xs:time): in a union ofxs:dateandxs:dateTimea fixed 2026-11-04 may be restricted to 2026-11-04T00:00:00. The comparison is one-sided as in Xerces: when the value of the declaration or the base is agMonthorgMonthDaythe year is not compared, for agDayorxs:timeneither year nor month (a fixed--01may be restricted to 2026-01-01, not the other way round). In documents (fixed values, enumerations, identity constraints) such values stay different, as in XML Schema 1.0 (no test of the suite covers this; in the source code the mark readsXerces deviation, not in the test suite). - An
xs:timeof 24:00:00 is 00:00:00 of the next day, as Xerces gives every time the same placeholder date: it differs from 00:00:00 and comes after 23:59:59 (in enumerations, ranges, fixed values and identity constraints), and a default or fixed value of 24:00:00 becomes 00:00:00 (its canonical form); XML Schema 1.0 makes 24:00:00 the same time of day as 00:00:00. Forxs:dateTimeboth agree: T24:00:00 equals 00:00:00 of the next day (no test of the suite covers this; in the source code the mark readsXerces deviation, not in the test suite). - A value that fails a facet other than pattern is still the value of
identity constraints (a failed list item gives its own value, or that
of the item before it; a union no member type matches gives that of
the last member type that had one), so equal invalid values are
duplicates; XML Schema gives an invalid value no actual value (no test
of the suite covers this; in the source code the mark reads
Xerces deviation, not in the test suite). - An attribute matched by an attribute wildcard is a wild ID also when
its type is a list of ID items or a union with an ID member type, as
Xerces' notion of an ID type, so a second one is cvc-complex-type.5.1
and one beside such an attribute use cvc-complex-type.5.2; XML Schema
1.0 counts only
xs:IDand types derived from it (no test of the suite covers this; in the source code the mark readsXerces deviation, not in the test suite). xsi:schemaLocationandxsi:noNamespaceSchemaLocationhints in documents are never read: the schema is the one given to the loader, as with a preloaded schema in Xerces (the setup of the KoSIT validator), so tests that rely on hints fail (schA1,ctL021,attgD034,particlesB013, ElemDecltargetNS00101m1_p,ST_targetNS00101m2_p).
Differences from Xerces, also on purpose:
- Every
xs:importof a namespace is loaded; Xerces loads only the first one. - The seconds of dates, times and durations are exact decimals; Xerces uses doubles, so values with more than about 15 significant digits may compare differently.
- An
xs:ENTITYvalue must name an unparsed entity declared in the internal subset of the document's DOCTYPE (also through an internal parameter entity); the external subset is never read (no DTD processing), so an entity declared only there is reported as undeclared, and attribute defaults of the DTD are not added. The internal subset is read when the first ENTITY value is checked: a parameter entity that refers to itself raisesxecSyntax, one that nests deeper than 64 levels or expands to more than 1 MiBxecLimit. - The loader checks each schema document against the schema for schemas
(attributes, their values, children and their order) and the facets of
each simple type against each other and against its base type (as
Xerces, also fixed facets), Unique Particle Attribution of content
models, the rules of element and attribute declarations (value
constraints, ID types, consistent element types) and of type
derivations (
final, the content types of extensions and restrictions, the attributes, attribute wildcards and particles of restrictions) and of references, imports and redefinitions; schemas that Xerces rejects for a schema component constraint it does not check may load (the target schemas are valid for Xerces).
- The W3C XML conformance test suite is
run with
tools/run-xmlconf.pyand cross-checked against Python's expat: 1965 applicable tests, 1482 pass, 0 unexplained failures; the rest are the documented DTD and encoding decisions. Every accepted test document is also written back through the writer and through the DOM and must read back unchanged. - The W3C XPath/XQuery test suite QT3
measures how much of XPath 2.0 the evaluator covers (
tools/run-qt3.py, pinned suite revision): of 15,149 applicable XPath 2.0 cases 15,125 pass (99.8 %) and 9 more raise an error where one is expected, but with another code. The passing cases are a baseline: the CI fails if one of them stops passing. The evaluator has two profiles (TLandrixXmlXPathStaticContext.Profile):xppXPath20(the default) follows XPath 2.0 where Saxon, which runs XPath 2.0 rule sets as XPath 3.1, differs, e.g.roundwith two arguments is an error;xppSaxonbehaves as Saxon in the KoSIT validator and is what the Schematron validator uses; it also accepts the XPath 3.0 string concatenation operator||, which XSLT 2.0 stylesheets run by Saxon use. Both profiles have a QT3 baseline; the differences are tested intests/data/xpath/profiles.txt. - The KoSIT XRechnung test suite
(
tools/run-kosit-suite.py, pinned release 2026-08-31 with the matching KoSIT validator configuration): all 86 invoices are validated with the UBL or CII XML schema and the EN16931 and XRechnung rule sets of the scenario the KoSIT validator chooses. The schema verdict with error codes and positions is the KoSIT validator's, and fired rules, failed asserts, reports, messages and locations match Saxon with the validator's XSLT line by line, for all 86 invoices. All of them are schema-valid, so schema errors are not exercised here; the W3C XML Schema Test Suite below covers them. The full KoSIT report (assessment, custom levels) is not part of this library. - The W3C XML Schema Test Suite
(
tools/run-xsts.py, pinned revision) checks the XML Schema validation: of its 39,336 applicable XSD 1.0 tests (schemas that must load or be rejected, documents that must be valid or invalid) 39,228 pass and none uses a construct this library does not support (such a construct, a selector with an attribute step before its last step, is reported as unsupported, never accepted silently). Of the 108 failures, 104 are the cases of the list above, where the result of Xerces is given on purpose; 3 are XML 1.1 documents, read with the rules of XML 1.0 (xv003,xv006,xv008); and one has a schema whose verdict the suite itself disputes (ste110, a circular union, which XML Schema 1.0 and Xerces reject). The passing tests are a baseline for the CI. - The XML Schema reference data (simple types, validator cases, content
models, Unique Particle Attribution, the schema rules in
tests/data/xsdschema/rules.txt) comes from the validator of the Java runtime, the one the KoSIT validator uses;tests/data/xerces-oracle.txtnames the runtime.tools/check-xerces-drift.py --jdk DIRregenerates it with another JDK and reports what changed, so a new Java version that changes Xerces' results is noticed. - About 135 test cases with more than 9000 checks, including generated
conversion cases with Python's
decimalmodule as an independent oracle. - Real-world XRechnung, ZUGFeRD and GAEB files (kept locally, not in the repository) are round-tripped through the DOM, and damaged copies must be rejected cleanly.
tools/fuzz.pyderives randomly damaged documents from the test files and compares the verdict with expat; every variant must either be read or be rejected withELandrixXmlError, and accepted ones must survive the writer and DOM round trips. 50,000 variants per run, built with range and overflow checking: no crash, no other exception, no unexplained difference.
Measured with tools/perf.ps1 on a 47 MB indented GAEB-like document
(3.8 million tokens):
| Delphi 13 Win64 | Free Pascal 3.2.2 Win64 | Delphi 6 | |
|---|---|---|---|
| Pull reader | 0.3 s | 0.5 s | 0.3 s |
| DOM load | 1.1 s | 1.6 s | 1.4 s |
| DOM save | 1.1 s | 1.4 s | 1.1 s |
| Peak memory with the DOM | 448 MB | 445 MB | 320 MB |
The peaks include the generated test document in memory several times (source, reader input, saved output). The peak of a run with the pull reader only was about 110 MB, most of it the generated document itself.
Tested, also with overflow and range checking enabled:
- Delphi 6, Delphi 7, Delphi 13 (Win32 and Win64); Delphi 13 macOS ARM64 compiles without warnings (not run)
- Free Pascal 3.2.2 on Windows (i386, x86_64), Linux (aarch64; x86_64 in CI)
- Free Pascal 3.2.4 on Linux (aarch64) and macOS (aarch64)
- Free Pascal 3.3.1 on Windows (aarch64) and Linux (aarch64)
Delphi 2007 and Delphi 2009 to 12 are expected to work but are not tested.
./tools/build.ps1 # minimum set: FPC 3.2.2, Delphi 6, Delphi 13 Win32
./tools/build.ps1 all # every local toolchainOr directly with Free Pascal:
mkdir -p build/fpc
fpc -B -Fusource -Fisource -FUbuild/fpc -FEbuild/fpc tests/LandrixXmlTests.dpr
./build/fpc/LandrixXmlTestsMIT, see LICENSE. The generated Unicode, XML 1.0 and case tables and the Xerces-compatible XML Schema validation are derived from third-party material; see THIRD-PARTY-NOTICES.md.