Package: @uncefact/untp-test-suite v0.1.0 (monorepo uncefact/tests-untp @ d09f538, tag reference-implementation-v0.3.0)
Tier: 2 (UNTP schema validation)
Severity: Tier 2 is unusable for any UNTP v0.7.0 credential.
Summary
In UNTP v0.7.0 the per-type JSON-LD context URLs were replaced by a single unified
context, https://vocabulary.uncefact.org/untp/0.7.0/context/ (see the RI v0.7.0
migration guide). The Tier 2 schema mapper still derives the schema URL from the
0.6.x context scheme (https://test.uncefact.org/vocabulary/untp/<abbr>/<version>/),
so it cannot extract a version from a 0.7.0 credential. getSchemaUrlForCredential
returns null and the test fails at the guard assertion, never reaching the actual
schema validation.
This is independent of schema availability: the official v0.7.0 schemas are
published (under https://untp.unece.org/artefacts/schema/v0.7.0/...), just not at
the path the mapper derives.
Reproduction (minimal)
Any DPP whose @context uses the unified 0.7.0 context:
npx untp-test --tag tier2 ./dpp-0.7.0.json
Observed:
Tier 2 - UNTP Schema Validation (tags: tier2, untp)
dpp-0.7.0.json
✖ should validate against DigitalProductPassport UNTP schema (tags: schema)
Should be able to determine UNTP schema URL for credential: expected null to be a string
Proof the credential is actually valid against the official 0.7.0 schema.
Using the suite's own validation path (createAjvInstance + validateJsonAgainstSchema)
pointed at the official schema, the same credential passes:
const { createAjvInstance } = require('@uncefact/untp-test-suite/dist/untp-test/utils.js');
const { validateJsonAgainstSchema } = require('@uncefact/untp-test-suite/dist/untp-test/test-utils.js');
const cred = require('./dpp-0.7.0.json');
const ajv = createAjvInstance();
const res = await validateJsonAgainstSchema(
cred,
'https://untp.unece.org/artefacts/schema/v0.7.0/dpp/DigitalProductPassport.json',
ajv,
);
console.log(res.valid); // -> true
Root cause
Two places hardcode the 0.6.x context/URL scheme:
src/untp-test/schema-mapper/default-mappings.json — every versionRegex matches
only https://test\.uncefact\.org/vocabulary/untp/<abbr>/([^/]+)/, and every
schemaUrlPattern points at test.uncefact.org/.../untp-<abbr>-schema-{version}.json.
src/untp-test/test-utils.ts::extractUNTPVersion (lines ~294–305) builds the same
test.uncefact.org/vocabulary/untp/(<abbr>)/([^/]+)/ regex.
There is also a selection bug that blocks the obvious workaround (adding a 0.7.0
mapping entry): src/untp-test/schema-mapper/loader.ts::getSchemaUrlFromMappings
selects the mapping with mappings.find(m => m.credentialType === targetType) — the
first entry for that type — and if its versionRegex doesn't match, returns null
instead of trying the next matching entry. So a second (0.7.0) mapping for the same
credentialType is never reached.
Suggested fix
(a) Make mapping selection fall through on non-match so multiple version schemes
can coexist (loader.ts):
- // Find matching mapping by credential type
- let matchedMapping: SchemaMappingConfig | undefined;
- if (credential.type.includes(targetType)) {
- matchedMapping = mappings.find((mapping) => mapping.credentialType === targetType);
- } else {
- return null;
- }
- if (!matchedMapping) {
- return null;
- }
- if (!matchedMapping.versionRegex) {
- return matchedMapping.schemaUrlPattern;
- }
- const credentialString = JSON.stringify(credential);
- const versionMatch = credentialString.match(new RegExp(matchedMapping.versionRegex));
- if (!versionMatch || !versionMatch[1]) {
- return null;
- }
- const version = versionMatch[1];
- return matchedMapping.schemaUrlPattern.replace('{version}', version);
+ if (!credential.type.includes(targetType)) {
+ return null;
+ }
+ const credentialString = JSON.stringify(credential);
+ // Try every mapping for this type; return the first that resolves a URL.
+ for (const mapping of mappings.filter((m) => m.credentialType === targetType)) {
+ if (!mapping.versionRegex) {
+ return mapping.schemaUrlPattern;
+ }
+ const versionMatch = credentialString.match(new RegExp(mapping.versionRegex));
+ if (versionMatch && versionMatch[1]) {
+ return mapping.schemaUrlPattern.replace('{version}', versionMatch[1]);
+ }
+ }
+ return null;
(b) Add 0.7.0 mappings (unified context → untp.unece.org/artefacts schemas) in
default-mappings.json, kept alongside the existing 0.6.x entries:
Heads-up for maintainers: the v0.7.0 DCC artefact breaks the Digital<Type> filename
convention used by the other four types — it is published as ConformityCredential.json,
not DigitalConformityCredential.json. Worth aligning upstream, or at least documenting,
to avoid the same 404 in other tooling.
(c) Update extractUNTPVersion to also recognise the unified context, e.g. add a
second regex https://vocabulary\.uncefact\.org/untp/([^/]+)/context/ and return the
first capture group that matches.
Verified: all five v0.7.0 schema artefacts return HTTP 200, with sha256:
dpp/DigitalProductPassport.json — 42c51943ab23547d5287899fd12b214b19b006c28d105a70ff390f8551b12653
dcc/ConformityCredential.json — 3fef068faece5975daa799c8f2d1db675c902f9a4e2c6d3c5fb6833799a73af1
dfr/DigitalFacilityRecord.json — a887dd1bd50e01d5aff6c7fb62b92d90b355f34378f2ff4cae2fdd1b032a614e
dia/DigitalIdentityAnchor.json — e26ec4543b29b353e720b345671ccf4926d472c2c5e1c65ecb6f5e9dd6e2a71d
dte/DigitalTraceabilityEvent.json — a1fa17b743d729e5cb7d1e7db689b87cc0bc655395a625b7ae6f9d195ecc84f9
Implementer-side note (Reeco, 2026-06-11)
Verified independently against real UNTP v0.7.0 DPPs (issuer did:web:ia.reeco.eco): after
adding a Reeco extension @context as the third array element, the credentials pass Tier 1
(valid JSON-LD + W3C VC schema) and pass the official UNTP v0.7.0 schema (Tier 2 diagnostic,
via the suite's own createAjvInstance + validateJsonAgainstSchema). The only remaining
failures are strictly suite-side — this issue — i.e. they are not a property of the
credentials under test.
Package:
@uncefact/untp-test-suitev0.1.0 (monorepouncefact/tests-untp@d09f538, tagreference-implementation-v0.3.0)Tier: 2 (UNTP schema validation)
Severity: Tier 2 is unusable for any UNTP v0.7.0 credential.
Summary
In UNTP v0.7.0 the per-type JSON-LD context URLs were replaced by a single unified
context,
https://vocabulary.uncefact.org/untp/0.7.0/context/(see the RI v0.7.0migration guide). The Tier 2 schema mapper still derives the schema URL from the
0.6.x context scheme (
https://test.uncefact.org/vocabulary/untp/<abbr>/<version>/),so it cannot extract a version from a 0.7.0 credential.
getSchemaUrlForCredentialreturns
nulland the test fails at the guard assertion, never reaching the actualschema validation.
This is independent of schema availability: the official v0.7.0 schemas are
published (under
https://untp.unece.org/artefacts/schema/v0.7.0/...), just not atthe path the mapper derives.
Reproduction (minimal)
Any DPP whose
@contextuses the unified 0.7.0 context:{ "@context": [ "https://www.w3.org/ns/credentials/v2", "https://vocabulary.uncefact.org/untp/0.7.0/context/" ], "type": ["DigitalProductPassport", "VerifiableCredential"], "issuer": { "type": ["CredentialIssuer"], "id": "did:web:example.org" }, "validFrom": "2026-01-01T00:00:00Z", "credentialSubject": { "type": ["Product"], "name": "Example" } }Observed:
Proof the credential is actually valid against the official 0.7.0 schema.
Using the suite's own validation path (
createAjvInstance+validateJsonAgainstSchema)pointed at the official schema, the same credential passes:
Root cause
Two places hardcode the 0.6.x context/URL scheme:
src/untp-test/schema-mapper/default-mappings.json— everyversionRegexmatchesonly
https://test\.uncefact\.org/vocabulary/untp/<abbr>/([^/]+)/, and everyschemaUrlPatternpoints attest.uncefact.org/.../untp-<abbr>-schema-{version}.json.src/untp-test/test-utils.ts::extractUNTPVersion(lines ~294–305) builds the sametest.uncefact.org/vocabulary/untp/(<abbr>)/([^/]+)/regex.There is also a selection bug that blocks the obvious workaround (adding a 0.7.0
mapping entry):
src/untp-test/schema-mapper/loader.ts::getSchemaUrlFromMappingsselects the mapping with
mappings.find(m => m.credentialType === targetType)— thefirst entry for that type — and if its
versionRegexdoesn't match, returnsnullinstead of trying the next matching entry. So a second (0.7.0) mapping for the same
credentialTypeis never reached.Suggested fix
(a) Make mapping selection fall through on non-match so multiple version schemes
can coexist (
loader.ts):(b) Add 0.7.0 mappings (unified context →
untp.unece.org/artefactsschemas) indefault-mappings.json, kept alongside the existing 0.6.x entries:{ "credentialType": "DigitalProductPassport", "versionRegex": "https://vocabulary\\.uncefact\\.org/untp/([^/]+)/context/", "schemaUrlPattern": "https://untp.unece.org/artefacts/schema/v{version}/dpp/DigitalProductPassport.json" } // ...and equivalently for dcc/dfr/dia/dte. Note the published filenames are NOT uniform: // dfr/DigitalFacilityRecord.json, dia/DigitalIdentityAnchor.json, dte/DigitalTraceabilityEvent.json // follow the Digital<Type> convention, but dcc is published as dcc/ConformityCredential.json // (no "Digital" prefix), e.g. // .../v{version}/dcc/ConformityCredential.json // so each type needs its own explicit schemaUrlPattern; a single Digital{Type}.json template // would 404 on dcc.(c) Update
extractUNTPVersionto also recognise the unified context, e.g. add asecond regex
https://vocabulary\.uncefact\.org/untp/([^/]+)/context/and return thefirst capture group that matches.
Implementer-side note (Reeco, 2026-06-11)
Verified independently against real UNTP v0.7.0 DPPs (issuer
did:web:ia.reeco.eco): afteradding a Reeco extension
@contextas the third array element, the credentials pass Tier 1(valid JSON-LD + W3C VC schema) and pass the official UNTP v0.7.0 schema (Tier 2 diagnostic,
via the suite's own
createAjvInstance+validateJsonAgainstSchema). The only remainingfailures are strictly suite-side — this issue — i.e. they are not a property of the
credentials under test.