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.

Create a free account to save this page, take notes and track your progress. Sign in

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

A closed schema vs an open oneTry it yourself
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.

Was this page helpful?