# 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](/conformance/) |
| **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:

```json
"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:

```json
"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 CBV 2.0 §8.9 reference vectors (RalphTro/epcis-event-hash-generator). `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](/translate/), the pinned schema digests on [/conformance/](/conformance/), and when the corpus is clean and you want the write half — a durable destination for the dual-write — [the key list](/get-a-key/?segment=translate) is where that opens.

---

**Proof, not adjectives.** This build could not verify the gateway's test suite green, so no sentence on this page claims a passing run — the last verified run is recorded in the repository's own CI, not here. Live on this origin, no key: POST /translate, /validate, /hash. No conformance attestation has ever been issued. The dated ledger is /what-ships-today/.
