XML in Production · Security and Integration
The Partner's New Field Broke Our Validator
A partner added an optional element to its messages. The receiver's strict schema rejected every message, stopping order flow for six hours.
Scenario
Example team: a logistics company receives shipment notices from carriers, validated against an XSD written by the company. A carrier adds an optional <carbonKg> element to every notice, announced in a newsletter nobody read.
What you see in production
Every notice from that carrier is rejected with "Element 'carbonKg': This element is not expected". Tracking pages show parcels stuck in "label created" for six hours, until the carrier is asked to roll back.
The naive code
<xs:sequence>
<xs:element name="tracking" type="xs:string"/>
<xs:element name="status" type="xs:string"/>
</xs:sequence> <!-- nothing else allowed, ever -->
Reproduce it
from lxml import etree
def schema(open_content):
extra = '<xs:any namespace="##any" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>' if open_content else ""
return etree.XMLSchema(etree.fromstring(f"""<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="notice"><xs:complexType><xs:sequence>
<xs:element name="tracking" type="xs:string"/>
<xs:element name="status" type="xs:string"/>
{extra}
</xs:sequence></xs:complexType></xs:element>
</xs:schema>""".encode()))
v1 = b"<notice><tracking>ZX123</tracking><status>in_transit</status></notice>"
v2 = b"<notice><tracking>ZX123</tracking><status>in_transit</status><carbonKg>1.2</carbonKg></notice>"
broken = b"<notice><status>in_transit</status></notice>"
for name, s in (("closed schema", schema(False)), ("open schema", schema(True))):
results = [("valid" if s.validate(etree.fromstring(d)) else "invalid") for d in (v1, v2, broken)]
print(f"{name}: v1={results[0]}, v2 with new field={results[1]}, missing tracking={results[2]}")
Output
closed schema: v1=valid, v2 with new field=invalid, missing tracking=invalid open schema: v1=valid, v2 with new field=valid, missing tracking=invalid
Root cause
The schema encoded "exactly these elements and nothing else", so a backward-compatible addition was treated as an error. Validation was strict on things that don't matter (unknown extras) and should be strict on things that do (required fields, types).
The fix
- Design for extension: add an
xs:any processContents="lax"wildcard at the end of sequences (or use namespaces for extensions), and have consumers ignore unknown elements: the tolerant reader pattern. - Version the contract: put the version in the namespace (
urn:carrier:notice:v2) or an attribute, and support the previous version for a deprecation period. - Get partners' schema changes into CI: download published XSDs and test sample files against them nightly.
- Quarantine invalid messages for replay instead of dropping them.
Trade-offs
- Open content lets typos through (
<stauts>is "unknown" rather than an error); keep required elements strict. - Supporting two versions at once costs code, but avoids synchronised big-bang releases between companies.
Detect it in production
- Rejected-message counts per partner and per error type, with alerts on spikes.
- A dead-letter queue with replay tooling.
Key points
- Be strict about what you need, tolerant of additions.
- Version contracts, test partners' changes continuously, and quarantine rather than drop.
Practice questions
How do you make an XSD tolerant of future optional elements?
Answer
Add a wildcard such as <xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> at extension points, and write consumers that ignore unknown elements (tolerant reader).
Open in the app to mark lessons complete, take notes and track your progress.