The launch journal

EPCIS 1.2 XML to 2.0 JSON-LD, deterministic, with a per-job fidelity report.

31 July 2026 · the launch journal

You own a decade of vendor XML — 1.1 and 1.2, some of it from systems whose vendors no longer exist — and you care about exactly one thing: does it round-trip without loss, and can you prove it per job, not per press release. So here is the translator's contract in one sentence, and then the receipts: loss may exist, but silence about it may not.

Every translate job returns three things together: the 2.0 JSON-LD document, a fidelity report computed by recomputation against your source, and the schema verdict against the pinned official EPCIS 2.0.1 schema. A job is delivered only if it is round-trip clean and schema-valid; otherwise it fails visibly and is never delivered. There is no partially-trusted output state.

Job one: the clean case, verbatim.

golden-corpus/xml-translation/object-event-1.2.xml is a committed 1.2 fixture — namespaced XML, bizStep as a urn:epcglobal:cbv URI, a po business transaction, source/destination PGLNs. One command:

$ npx epcis.dev translate object-event-1.2.xml --json

The delivered document (abridged to the load-bearing fields):

{
  "status": "delivered",
  "document": {
    "@context": "https://ref.gs1.org/standards/epcis/epcis-context.jsonld",
    "type": "EPCISDocument",
    "epcisBody": { "eventList": [{
      "type": "ObjectEvent",
      "eventTime": "2020-06-07T17:10:16Z",
      "epcList": ["urn:epc:id:sgtin:0614141.107346.2017",
                  "urn:epc:id:sgtin:0614141.107346.2018"],
      "action": "OBSERVE",
      "bizStep": "shipping",
      "disposition": "in_transit",
      "bizTransactionList": [{ "type": "po",
        "bizTransaction": "http://transaction.acme.com/po/12345678" }]
    }]}
  },
  "fidelity": {
    "roundTripClean": true,
    "lossyPaths": [],
    "notes": "Clean round-trip: every source value maps to a standard EPCIS 2.0 field."
  },
  "sourceSchemaVersion": "1.2"
}

Note what the translator did without being asked: CBV URIs migrated to 2.0 bare-word form (urn:epcglobal:cbv:bizstep:shippingshipping), the 1.2 envelope became a 2.0 EPCISDocument under the official JSON-LD context, and sourceSchemaVersion records what it was handed. All of it deterministic: same bytes in, same bytes out, today and in five years. Determinism is what makes the fidelity report evidence rather than a mood — a flagged job stays flagged on re-run, and a clean job cannot go quietly unclean between releases.

Job two: the one the report flags.

The honest half of the contract is the job that is not perfectly mappable. transformation-event-1.2-ext.xml carries a vendor extension element — example:internalRef — that has no standard 2.0 home. The translator does not drop it, and does not silently normalize it. It preserves the value under the extension mechanism and declares the path:

{
  "status": "delivered",
  "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."
  },
  "sourceSchemaVersion": "1.2"
}

Read the mechanics behind roundTripClean, because this is the part you should be skeptical about: it is computed by recomputation, not bookkeeping. The checker canonicalizes every leaf value of the source event body (applying the same CBV and number migrations the translator applies), canonicalizes every leaf of the output, and verifies the source multiset is contained in the output. The translator does not get to grade its own homework from its own notes — a mapped value that fails to arrive is a translator defect, the job fails, and it is never delivered. lossyPaths then enumerates every source path that crossed the boundary as an extension rather than a core field, so the loss ledger is part of the output, not a caveat in the docs.

Why per-job beats per-release.

Most migration tools publish fidelity the way vendors publish uptime: a suite passed once, somewhere, on fixtures you never saw. But your corpus is not our test corpus. Your 2014 files have the namespace quirks of a WMS that was end-of-lifed in 2019; your co-packer's 1.1 documents nest extensions in ways no golden fixture anticipated. A per-release claim tells you the translator was healthy; only a per-job report tells you your document survived. That is why the report is emitted on every job, why the corpus pairs that gate the translator are committed source→expected, byte-compared in CI, and why the output side is held to the pinned official schema — the same digests you can re-verify against ref.gs1.org yourself.

Run a decade of it this afternoon.

The evaluation is the first value, and it costs you nothing but the files you already have:

$ npx epcis.dev translate legacy/2016/*.xml --json

— or POST any single document to /translate on this origin, no key, no account, and read the fidelity block that comes back. Exit codes are stable and typed (0 delivered, 1 failed-visibly; the error surface is a contract too), so wiring it into a sweep script is an afternoon, and every flagged path lands in a list you can hand to whoever owns the downstream mapping.

No meeting. No account. Your corpus, our translator, and a receipt for every single job — which is the only basis on which a migration of ten years of evidence should ever be trusted.

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.