A chance remark at Balisage 2026 (RIP) led me to consider the question: can you write an XSD schema to validate a linked set of XML documents, rather than just a single document?
Here's a use case: the test suite for XQuery/XPath 4.0 consists of a catalog file, which lists 800 or so linked test sets. To make it more specific, the catalog contains many lines rather like this:
<test-set name="bin-shift" file="bin/shift.xml"/>
Both the top-level catalog and the individual test-set files are governed by XSD schemas. Currently we validate the collection of documents using an XQuery query along the lines of:
//fots:test-set/@file ! (validate{doc(resolve-uri(., base-uri(..)))})
And of course other solutions are possible, for example using XProc. But it occurred to me, can we write a schema for the catalog file that includes an XSD 1.1 assertion that invokes validation on the referenced test-set files? And can we use a different schema to validate the test-set files, different from the one we use for the catalog?
I'll give you the answer first, before describing the journey to get there. I've made it work like this
<xs:annotation>
<xs:appinfo>
<saxon:param name="test-set-validator"
select="xsd-validator(map{'schema-location': 'test-set-schema.xsd',
'use-imported-schema': false(),
'return-error-details': true()})"/>
</xs:appinfo>
</xs:annotation>
<xs:element name="test-set">
<xs:complexType>
<xs:complexContent>
<xs:extension base="baseType">
<xs:attributeGroup ref="nameAttr"/>
<xs:attributeGroup ref="fileAttr"/>
<xs:assert test="
let $doc := doc(resolve-uri(@file, base-uri(.)),
{'xsd-validation':'skip'})
let $validity := $test-set-validator($doc)
return if ($validity?is-valid)
then true()
else (false(), `In {@file}: {$validity?error-details?message => string-join('. ')}`)"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
</xs:element>
This won't run out-of-the-box in Saxon 13.0: though most of the ingredients are present I had to make some tweaks to make it all hang together, which I'll describe as I go along. It should work as written in 13.1.
Firstly, the saxon:param element has been available for quite a while as
a Saxon extension. I'm using it here not to parameterize the validation, but rather to
set up a global variable. The variable is the result of the 4.0 xsd-validator
function: a new higher-order function that, given a schema, returns a function that can be
used to validate instance documents against this schema. Putting this validator in a global
variable means we only need to compile the schema for the test-set documents once.
The expression in the xs:assert/@test attribute contains several features
of note:
- Firstly, it calls the
doc()function to load the test-set document referenced in the@fileattribute. We're loading this document because we want to pass it to the validation function. The wayxsd-validatorworks, you can only validate an already-built document tree. We want to do the validation using a different schema, so we can't do this within thedocfunction itself. - Invoking the validator function (
let $validity := $test-set-validator($doc)) returns a record containing potentially three things: a success/failure flag, a list of validation errors, and an annotated document representing the PSVI. We're not interested in the last part, so we didn't ask for it in the options to thexsd:validatorcall. - We need to know which test-set documents are invalid, so it's helpful to have customized
error message if the call on the validator function returns errors. There's an existing
Saxon extension,
saxon:message, which allows you to produce a custom message when an assertion fails, but unfortunately it's a fixed message, it can't be tailored to the actual data. So I invented a new mechanism. The XSD 1.1 specification says that if the result of the assertion has no effective boolean value, the assertion is taken as failing. So you can now return the sequence(false(), "message")from an assertion. This has no effective boolean value, so it is equivalent to returning false, but it also returns a diagnostic message explaining what is wrong. (The backtick expression is a string template, a new addition in XPath 4.0). - XSD 1.1 expects assertions to use XPath 2.0 syntax but there is a Saxon attribute
saxon:extensions="any-xpath-version"to relax this rule. (I found that this was only allowing 3.1 syntax, not 4.0, but this is now fixed). - Finally, XSD 1.1 says that you can't call the
doc()function from within an assertion. This is a security mechanism. So I added a new configuration option--assertionsCanSeeResources:onto bypass this mechanism: only set this option if you are using a trusted schema.
So with Saxon 13.1, you will be able to invoke validation of a linked set of documents, using the same schema or different schemas, in a single call of the validation API.