Document ID Migration#
Overview#
Strapi 5 replaced numeric id as the canonical content identifier with documentId — a 24-character alphanumeric string that is stable across locales, draft/publish variants, and data transfer operations. In Strapi v4, a single piece of content could have multiple numeric IDs (one per locale × publication state), making IDs fragile across operations like duplication and export/import. documentId groups all variants of a content entry under one persistent identifier .
This migration is a documented breaking change affecting:
- Content API (REST & GraphQL): responses now return
documentIdinstead ofid - Document Service API: replaces Entity Service; all operations accept
documentId - Transform layer: internal two-phase extract-then-transform that resolves
documentId→ internal entityidbefore writing to the database - Data transfer: import/restore must correctly identify which relation refs are numeric IDs versus
document_idstrings
The upgrade tool codemod partially automates the Entity Service → Document Service migration, but cannot guess documentId values for existing content .
Transform Layer: ID Map and Relation Mapping#
The Document Service transform layer bridges the public documentId API surface and the internal database id. It operates in two phases for every write operation:
Phase 1 — Extract: extractDataIds traverses all relation attributes in the incoming payload and calls IdMap.add() for each documentId encountered, keyed on { uid, documentId, locale, status }.
Phase 2 — Transform: transformDataIdsVisitor traverses the same payload again and replaces each documentId with the corresponding internal entity id returned by IdMap.get(). If a documentId cannot be resolved and allowMissingId is not set, a ValidationError is thrown .
IdMap (id-map.ts) is the registry backing both phases. It batches database lookups by uid + locale to avoid N+1 queries , and encodes compound keys as "key:::value&&key:::value" strings for use in a Map . The status field is omitted from the key for content types that do not have Draft & Publish enabled .
mapRelation (utils/map-relation.ts) normalises all supported relation input formats before the transform callback fires:
| Input format | Normalised to |
|---|---|
Numeric shorthand (5) | { id: 5 } |
String shorthand ("abc123") | { documentId: "abc123" } |
Object with id or documentId | Passed directly to callback |
{ set, connect, disconnect } | Each key recursively mapped |
| Array | Each item recursively mapped, result wrapped in { set: [...] } |
The final output of mapRelation is always { set, connect, disconnect } for top-level calls .
traverseEntityRelations wraps the generic traverseEntity utility to skip non-relation attributes, join-column relations (useJoinTable === false), and morphToOne relations (which are handled separately via inline columns).
Data Transfer Compatibility: Localization Link Fix#
The problem: The i18n localizations relation is a virtual relation implemented as a joinColumn whose referencedColumn is document_id (a string), not the numeric id. During export, this ref is emitted as a string. During import/restore, the link writer was passing every ref through a numeric mapID function, which returned undefined for string document IDs. This produced false-positive warnings for every localization link, even though the data was valid .
The fix (PR #26870): A new resolve-link-ref.ts module was added with two exports:
isDocumentIdJoinColumnTarget— inspects the DB metadata for the left-side type and field; returnstrueif the relation's join column referencesdocument_idrather thanid.resolveLinkRef— returns the ref unchanged fordocument_idjoin column targets; otherwise converts the ref to a number and runs it throughmapID.
createLinksWriteStream in links.ts was updated to call resolveLinkRef for both left and right sides of every link instead of calling mapID directly . A ref that resolves to undefined (genuinely missing transferred entity) still triggers the skip-with-warning path .
The ILink type's ref field was also broadened from number to number | string to reflect that refs can be either numeric row IDs or document_id strings .
Scope: This fix is narrowly targeted at joinColumn relations that reference document_id. Standard M2M and other join-table relations that use numeric row IDs are unaffected.
Key Files and References#
Transform Layer (packages/core/core/src/services/document-service/transform/)#
| File | Role |
|---|---|
id-map.ts | Registry mapping documentId → entity id; batched DB loading |
relations/extract/data-ids.ts | Phase 1: populate IdMap from payload |
relations/transform/data-ids.ts | Phase 2: replace documentId with entity id |
relations/utils/map-relation.ts | Normalises all relation input formats; traverseEntityRelations helper |
Data Transfer (packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/)#
| File | Role |
|---|---|
resolve-link-ref.ts | Detects document_id join column targets; bypasses numeric mapping for them |
links.ts | Link restore write stream; uses resolveLinkRef to handle mixed ref types |
Database Migration#
| File | Role |
|---|---|
migrations/database/5.0.0-discard-drafts.ts | v4→v5 DB migration; backfills draft rows; rewires all relations to new draft/publish model |