Querying the model¶
Everything on this page runs against
report.xsd.
Finding what a schema declares¶
A SchemaSet is a mapping over the global elements your documents declare.
len(schemas) # 1
"{urn:example}report" in schemas # True
list(schemas) # ['{urn:example}report']
schemas.items() # (name, element) pairs
dict(schemas) # a real mapping, so this works
Types and attributes are separate symbol spaces — an element and a type may share a name — so each kind has a view of its own. A view iterates and indexes by position like a list, and looks up by name like a mapping:
schemas.types["{urn:example}Money"] # <Type complex {urn:example}Money>
schemas.types.keys() # the five declared types, built-ins excluded
schemas.elements # [<Element {urn:example}report>]
schemas.attributes # global attributes, xml: and xsi: excluded
schemas.documents # what was read, with target namespaces
The lookup methods return None when there is nothing, for when absence is
an ordinary answer; subscripting raises KeyError, for when it is a
mistake.
Name lookups return a reference — a borrow of the schema plus an id, so
following one allocates nothing — or None when the schema declares no
such name.
use xsdkit::Schemas;
fn look_around(schemas: &Schemas) {
let report = schemas.element(Some("urn:example"), "report"); // Option<ElementRef>
let money = schemas.type_(Some("urn:example"), "Money"); // Option<TypeRef>
for e in schemas.global_elements() {
println!("{}", e.display_name());
}
for t in schemas.global_types() {
// Unlike Python's `schemas.types`, this includes the 50 built-ins:
// they are real components here, not a special case.
println!("{}", t.display_name());
}
}
When the id is what you mean to keep — as a map key, or to compare — the
_id forms hand it over directly, and Schemas::get goes back the other
way.
Walking the tree¶
An element behaves as its children, so you navigate a schema without a
.type hop at every level.
Sized, iterable and subscriptable by name.
[child.local_name for child in report]
# ['title', 'issued', 'item']
report["item"]["price"].type.qname
# '{urn:example}Money'
len(report) # 3
A bare local name is enough, because a child is almost always in its
parent's namespace. Clark notation and (namespace, local) pairs work too.
use xsdkit::Schemas;
fn walk(schemas: &Schemas) -> Option<()> {
let report = schemas.element(Some("urn:example"), "report")?;
let names: Vec<&str> = report.children().map(|c| c.local_name()).collect();
// ["title", "issued", "item"]
let price = report.child("item")?.child("price")?;
println!("{}", price.type_of().display_name());
// {urn:example}Money
Some(())
}
child takes a local name for the same reason Python's subscript does.
Turning a name into text never goes through the interner: local_name,
namespace and display_name are on every reference, and
Schemas::local_of and namespace_of do the same for a bare QName.
Read it once, whole
element.tree() prints the shape rather than making you walk it.
report: {urn:example}Report
@id
title: xs:string
issued: xs:date
item+: {urn:example}Item
@sku
@quantity?
price: {urn:example}Money
@currency
note?: xs:string
? optional, + one or more, * any number, nothing for exactly once;
@name for attributes. Recursion stops where the shape starts repeating,
so a self-referential schema prints rather than hangs. In a notebook the
same call renders as a colour-coded tree — see In a notebook.
Children come from everywhere¶
children is not "the elements written inside this type's xs:sequence". It
is every element that may actually appear there, with
- content inherited through
xs:extensionalready included, xs:groupreferences expanded,- substitution groups closed transitively, abstract heads skipped.
That resolution is the whole point of working against components. Doing it yourself from documents is where XSD tooling goes to die.
Occurrence belongs to the pair¶
How often a child may appear is a fact about the parent and child together,
not about the declaration, because the same element can be referenced with
different occurrence constraints in different places. So subscripting or
iterating a parent gives a Child: the declaration, plus how it may appear
here. A Child answers everything an Element does, and child.element
gets the bare declaration back.
This is exactly the pair of questions a table-versus-column decision needs when you are mapping a schema onto a relational or columnar shape.
Ask for all of them at once
Both facts come from walking the content model, and asking for the
children walks it once for the whole type. In Rust the per-child
predicates Schemas::child_repeats and Schemas::child_is_optional are
still there for a single question about a single child — but called in a
loop they re-walk the model per child, which on a type with hundreds of
them (ordinary in GML, UBL and WITSML) measures about 40× slower.
Attributes¶
You get attribute uses, not bare declarations: the use carries required,
default and fixed, because those belong to the place the attribute is used
rather than to the attribute itself. Attribute groups are already flattened in,
transitively, and so are the attributes inherited from base types.
Types¶
money = schemas.types["{urn:example}Money"]
money.is_complex # True
money.content # 'simple' — a simple value with attributes on it
money.base.qname # '{http://www.w3.org/2001/XMLSchema}decimal'
money.derivation # 'extension'
money.derives_from(schemas.type("http://www.w3.org/2001/XMLSchema", "decimal"))
# True
[t.qname for t in money.base_chain]
# ['{urn:example}Money', '…}decimal', '…}anyAtomicType', '…}anySimpleType', '…}anyType']
use xsdkit::Schemas;
fn types(schemas: &Schemas) -> Option<()> {
let money = schemas.type_(Some("urn:example"), "Money")?;
assert!(money.is_complex());
// Walking up stops on its own: `xs:anyType` is its own base, so `base`
// reports `None` there rather than looping.
let mut t = money;
while let Some(base) = t.base() {
println!("{}", base.display_name());
t = base;
}
Some(())
}
content is one of empty, simple, element-only, mixed. For simple
types, variety is atomic, list or union, with item_type and
member_types for the latter two, and primitive naming what it ultimately
reduces to.
Facets, composed¶
currency = schemas.types["{urn:example}Currency"]
currency.facets.enumeration
# ['EUR', 'USD', 'GBP']
schemas.types["{urn:example}Sku"].facets.patterns
# [['[A-Z]{2}-[0-9]{4}']]
facets gives the constraints in force, composed down the whole
restriction chain — a type that declares only maxLength still reports its
base's minLength. declared_facets gives only what this restriction step
wrote. The first is what validation applies; the second is what the schema
author typed.
Two composition rules are easy to get wrong and are worth knowing:
- Patterns OR within a step, AND across steps. That is why
patternsis a list of lists: the outer list is restriction steps, the inner one is the alternatives declared at that step. - The innermost enumeration wins. A restriction may only narrow.
Bounds and enumerations are the lexical forms the schema wrote, not typed
values, because a facet constrains the lexical space as much as the value
space. Put one through validate for the value.
Validating a single value¶
currency.validate("EUR") # 'EUR'
currency.is_valid("ZZZ") # False
currency.validate("ZZZ")
# InvalidValueError: enumeration: `ZZZ` is not one of the 3 permitted values
schemas.type("http://www.w3.org/2001/XMLSchema", "date").validate("2024-12-01")
# datetime.date(2024, 12, 1)
validate applies whiteSpace first, then parses, then checks the composed
facets, and returns the value as its closest native Python type. This is the
same machinery the document validator uses, so a value that passes here passes
there.
An xs:QName is whatever its prefix is bound to where it was written, so give
the bindings, with "" for the default namespace:
qname = schemas.type("http://www.w3.org/2001/XMLSchema", "QName")
qname.validate("ex:report", namespaces={"ex": "urn:example"})
# '{urn:example}report'
A complex type with simple content, such as a price with a currency, validates
against the simple type of that content, and its facets are that type's.
Does this sequence fit?¶
N = "{urn:example}"
report.type.accepts([N+"title", N+"issued", N+"item"]) # True
report.type.accepts([N+"title", N+"issued", N+"item", N+"item"]) # True
report.type.accepts([N+"issued", N+"title", N+"item"]) # False — order
report.type.accepts([N+"title", N+"issued"]) # False — item required
Answered by running the compiled content automaton, not by pattern-matching particles. Rust says the same thing the same way:
use xsdkit::Schemas;
fn title_alone_is_enough(schemas: &Schemas) -> Option<bool> {
let report = schemas.element(Some("urn:example"), "report")?;
let title = schemas.qname(Some("urn:example"), "title")?;
Some(report.accepts([title]))
}
The matcher underneath is available too, for stepping through a document and
asking accepts_end() when you reach the end rather than judging a whole
sequence at once:
use xsdkit::{Schemas, TypeId};
fn step_through(schemas: &Schemas, ty: TypeId) -> Option<bool> {
let mut m = schemas.match_content(ty)?;
let title = schemas.qname(Some("urn:example"), "title")?;
Some(m.step(title) && m.accepts_end())
}
Content models compile to Glushkov position automata. Unique Particle
Attribution checking falls out of the same structure rather than being a
separate pass, and xs:all gets per-member counters instead of n! regex
paths.
Substitution groups¶
Already closed for you, and already reflected in the children, so an element with twelve substitutes shows all twelve as possible children of its parent.
Membership is not permission
block on a head bars substitution, or bars the derivation method a
member's type used to reach the head's. substitutes applies it — so it
answers what a document may actually name here, and agrees with both the
content model and the validator.
The other question, who is in the group, is
Schemas::substitution_group in Rust. It ignores block, so it can
report members that no document may use. Reach for it only when you mean
the group itself.
Next¶
- Validating documents — from a schema to a verdict and typed values.
- Python API — every method, with types.