Migrating a decade of 1.2 XML.
31 July 2026 · the launch journal
Somewhere in your object store is ten years of EPCIS 1.1 and 1.2 XML — vendor exports, partner feeds, the archive your traceability story actually rests on. A 2.0 mandate is arriving, and the question is not whether to migrate but in what order, and what you are allowed to lose. The answers: archive first, and nothing silently.
What actually changed
Strip the version-bump noise and EPCIS 2.0 is four concrete deltas against 1.2:
| delta | what it means for your corpus |
|---|---|
| JSON-LD is a first-class syntax | events carry an @context; XML remains valid, but the web-native form is JSON-LD and the two must translate deterministically |
| REST capture and query interfaces | the SOAP-era query surface gives way to HTTP interfaces with an official OpenAPI description — pinned on the conformance page |
| Sensor and certification extensions | sensorElementList and friends: dimensions your 1.2 events could not carry at all |
| GS1 Digital Link identifiers | the same physical identity now arrives as either an EPC URN or a Digital Link URI; the CBV 2.0 §8.9 hash's normalization exists so both compute the same event identity |
None of these rewrites your history's semantics. Which is exactly why the migration should start with history.
Archive first: translate and validate before touching production
The instinct is to wire the new pipeline first and backfill later. Resist it. The archive is the risk concentrate — every vendor quirk, every namespace invention, every timezone sin of the last decade lives there — and translating it is free, offline, and reversible. So the order is: translate and validate the archive first, dual-write second, cut over reads last.
Here is the first step on a real fixture from the golden corpus that ships with the package. golden-corpus/xml-translation/object-event-1.2.xml — a 1.2 shipping event with a po business transaction, owning-party source and destination — translates to 2.0 JSON-LD carrying the same epcList, bizStep: shipping, disposition: in_transit, the bizTransactionList with its po reference, and the PGLN source/destination pair, byte-for-byte deterministic. The job's fidelity report, reproduced from the run:
"fidelity": {
"roundTripClean": true,
"lossyPaths": [],
"notes": "Clean round-trip: every source value maps to a standard EPCIS 2.0 field."
}
Now the more interesting case — the one your archive is full of. transformation-event-1.2-ext.xml carries a vendor extension field with no standard 2.0 home. The report does not normalize it away:
"fidelity": {
"roundTripClean": true,
"lossyPaths": [ "/epcisBody/eventList/0/example:internalRef" ],
"notes": "Clean round-trip. 1 path(s) had no standard 2.0 home and were preserved
under the extension mechanism (declared loss, values retained):
/epcisBody/eventList/0/example:internalRef."
}
Read that carefully, because it is the trust contract of the whole migration: the path is named, the value is retained under the extension mechanism, and the loss ledger is part of the output — not a footnote in release notes, not a per-release claim about our test corpus. Your corpus is not our test corpus; a per-job report is the only honest grain. And the translated result is only delivered if it also validates against the pinned official EPCIS 2.0.1 schema — translation and conformance are one gate, not two hopes.
Dual-write, with the hash as the cross-era key
Once the archive translates clean (or with declared, reviewed losses), dual-write: production keeps emitting to the old store while every event also lands as 2.0 JSON-LD. The reconciliation key across eras is the CBV 2.0 §8.9 event hash — identity computed from event content, indifferent to whether the event arrived as 1.2 XML or 2.0 JSON-LD, checked here against OpenEPCIS reference vectors. POST /hash on this origin, or npx epcis.dev hash, gives you the ni:///sha-256 URI for any event; equal hashes across your two stores is the mechanical proof your dual-write is faithful. Cut reads over only when that equality holds across a full audit window.
The gotcha list, from the corpus
Three classes of surprise account for nearly everything the fidelity reports flag in legacy XML:
- Namespaces. Vendor extension namespaces survive translation under the 2.0 extension mechanism — declared, retained, flagged. What they never do is silently become standard fields.
- Timezones. 1.2's eventTimeZoneOffset is preserved exactly; events whose source systems disagreed about offsets will now disagree in a validator's face instead of in a recall.
- Extension survival. The projection law that governs this whole surface: extensions ride along, and the projection always validates against the pinned official schema. A superset that stops being conformant is not a superset; it is a fork.
Run it on your corpus this afternoon
POST /translate answers on this origin — application/xml in, 2.0 JSON-LD plus the fidelity report out, no key, no account. The same engine runs on your own bench with npx epcis.dev translate archive/*.xml --json, which is the right tool for a decade of files. The transcript idiom is on the translate page, the pinned schema digests on /conformance/, and when the corpus is clean and you want the write half — a durable destination for the dual-write — the key list is where that opens.
We answer in writing. We take at most five conversations a month, only when you ask for one, and only after you already have the written read.