Data Table row names, row handles, and stable IDs¶
"The item still exists in the table. Why did my old save lose it after I renamed the row?" The row's values survived; its identity did not.
The one-minute version¶
- A row name is the
Namekey used to address one row inside one Data Table.Potion_Smalldoes not identify a table by itself. - Unreal
Namevalues (FNamein C++) are case-insensitive and case-preserving. Do not create IDs that differ only by capitalization. - A Data Table Row Handle stores exactly two useful members: a Data Table object reference and a row name. It does not contain a copy of the row.
- A non-empty handle is not proof that the row still exists. Resolve it and handle failure whenever missing content is possible.
- Display name, localized text, icon, price, and description are row data, not identity. They may change or be duplicated without changing the ID.
- SaveGame and inventory state should usually persist a stable gameplay ID plus mutable state such as quantity. Look up the current definition after load instead of saving a stale copy of the whole row.
- Renaming a row is a data migration. Keep the old key, or ship an explicit
OldId -> NewIdmigration before removing it.
The three identities people mix together¶
Consider this entry:
Data Table asset: /Game/Data/DT_Items
Row Name: Potion_Small
Display Name: Small Health Potion
Those are three different values:
| Value | What it identifies | Safe to change casually? |
|---|---|---|
| Table asset reference/path | Which collection of rows | Moving through the editor usually updates serialized references, but path strings and external data need migration. |
| Row Name | Which row inside that table | No, not after gameplay, saves, or other assets depend on it. |
Display Name (Text) |
What the player sees | Yes. It can be edited or localized without changing identity. |
The full built-in row address is therefore:
(Data Table, Row Name)
A project-level ID may be shorter if the table is implied by the system:
Inventory definitions always use DT_Items
-> ItemId = Potion_Small is enough
If several tables share one namespace, include an explicit kind or registry:
DefinitionType = Item
DefinitionId = Potion_Small
DefinitionType = Ability
DefinitionId = Dash
Do not infer the table from a player-facing label or from whichever table happens to contain the key first.
Row names are map keys¶
UDataTable stores its rows in a map keyed by FName. That has practical
consequences:
- one table cannot contain two separately addressable rows with the same key;
- names that differ only by letter case compare as the same
FName; - row order is not identity and should not drive save data;
Noneis a poor gameplay ID because it also represents an unsetName;- punctuation and spaces may be technically representable, but simple stable machine names are easier to type, import, diff, and validate.
A useful naming scheme describes the concept, not its current presentation:
Good stable IDs
Potion_Small
Weapon_Rifle_Auto
Enemy_Guard_Heavy
Fragile IDs
Cheap Potion // price/presentation can change
Red Rifle // appearance can change
Item 17 // order is doing hidden identity work
For CSV imports, the configured key field (commonly the first Name column)
becomes the row name. For JSON imports, the configured/default key serves the
same role. Reimport rebuilds data from that source, so changing a key there is
still a row rename even if every other cell remains identical.
Get Data Table Row Names returns the current keys. Use it for editor tools,
debug output, or a small menu, not as a stable ordering contract. Sort an array
explicitly for display, and store the selected key rather than its array index.
What a Data Table Row Handle stores¶
FDataTableRowHandle is a small struct:
DataTableRowHandle
Data Table = DT_Items
Row Name = Potion_Small
The Data Table member is a serialized object reference to UDataTable; the
Row Name member is an FName. Make Data Table Row Handle combines those
members, and Break Data Table Row Handle exposes them again.
It does not store:
- the row's fields;
- a cached pointer to row bytes;
- the row struct type as a separate guarantee;
- a fallback row;
- a promise that the named row still exists.
This distinction matters:
Handle is not empty
does NOT imply
Handle resolves to a current row
IsNull in C++ checks whether the handle represents an empty selection. It is
not a database integrity check. A handle can name a row that was renamed or
removed after the property was authored. Resolve through the table and treat a
missing row as failure.
When a row handle is useful¶
Use a row handle when a designer-facing property should select both the table and row together:
BP_QuestReward defaults
RewardDefinition = (DT_Items, Potion_Small)
That is clearer than two unrelated editable variables and lets editor details panels offer a row picker after the table is selected.
The cost is coupling: the owner now has a hard reference to a particular table asset. Loading that owner can load the table, and moving to another definition source means changing every handle. For one small project this can be exactly the right tradeoff. It is not automatically the best SaveGame format.
What SaveGame and inventory should persist¶
Save the smallest durable statement about player state, not a snapshot of authored definition data:
FSavedInventoryEntry
ItemId = Potion_Small
Quantity = 3
Durability = 0.72 // only if this instance owns durability
On load:
for each SavedEntry:
ItemId = MigrateOldIdIfNeeded(SavedEntry.ItemId)
Get Data Table Row(DT_Items, ItemId)
if Row Found:
rebuild runtime inventory entry from current definition
restore Quantity / Durability from SaveGame
else:
record a missing-definition diagnostic
apply the project's explicit recovery policy
This keeps authored fixes effective. If version 1 saved a copied row with
MaxStack = 99, changing the table to MaxStack = 10 would not fix old
copies. Saving ItemId + Quantity lets every load use the current definition.
An original C++ save shape might be:
USTRUCT(BlueprintType)
struct FSavedInventoryEntry
{
GENERATED_BODY()
UPROPERTY(SaveGame)
FName ItemId = NAME_None;
UPROPERTY(SaveGame)
int32 Quantity = 0;
};
If one ID can refer to several definition domains, save a type/namespace too. If content comes from downloadable packs or mods, save the pack/primary asset identity required to resolve it. Do not silently guess.
Renames are migrations¶
Renaming a row¶
Serialized row handles, Blueprint Name defaults, Data Table cells, Data
Assets, and old save files can all contain the old key. Changing
Potion_Small to Consumable_Health_Small does not let those values discover
the new row automatically.
Use one of these deliberate strategies:
- Keep the old stable key. Change only display text and data. This is the cheapest and safest choice when the old ID is merely unfashionable.
- Migrate every authored reference and save. Ship a versioned map such as
Potion_Small -> Consumable_Health_Small, apply it before lookup, resave authored content, and retain migration long enough for supported saves. - Keep a compatibility row/alias. Useful temporarily, but make one canonical ID so two keys do not become two stack identities.
Core Redirects remap reflected classes, structs, properties, packages, and similar engine identities. A Data Table row name is your data key, so provide your own row-ID migration.
Moving or renaming the table asset¶
Manage assets through the Unreal Editor. Serialized asset references can be updated and redirectors can bridge unloaded packages until references are resaved. Raw strings outside that system do not gain the same protection.
Fix up redirectors and test a cooked build before deleting the old location. For long-lived external/save identity, a project-owned ID or Primary Asset ID is usually clearer than exposing a raw package path as gameplay meaning.
Reimport and localization¶
Reimport can add, remove, or rename keys and can replace field values. Validate the result as a schema/data change, not as harmless spreadsheet refresh.
Store player-facing labels as Text, not as row names. Two rows may both
display "Small Potion" in one language, and one row may display completely
different text in another language. Neither should affect lookup identity.
Failure policies belong to the owning system¶
Missing content should be visible, but recovery is a product decision:
| Context | Reasonable policy |
|---|---|
| Required startup definition | Fail validation loudly and stop that feature from starting. |
| Optional cosmetic reward | Skip it, log the full ID, and continue. |
| Removed inventory item in an old save | Migrate, replace, refund, or drop according to an explicit save-version rule. |
| Networked item ID unknown on one build | Reject the operation and diagnose build/content mismatch; do not invent local data. |
Never turn every missing ID into the first table row. That hides data loss and can grant the wrong item while making the original defect harder to reproduce.
Validate identity before shipping¶
For each definition domain, check:
- no row uses
Noneas its intended ID; - no source keys differ only by capitalization;
- every authored row handle resolves;
- every ID referenced by default inventory, quests, loot, or tests exists;
- display text is not being converted back into an ID;
- removed IDs have a migration or an explicit unsupported-save decision;
- CSV/JSON reimport reports no duplicate or missing key fields;
- a packaged build includes indirectly selected tables and definitions.
For a small game, an editor utility or automated test that iterates known references is enough. The important part is to validate before a player save becomes the first integrity test.
Lookalikes - which identity do I want?¶
| Value | Contains | Best use |
|---|---|---|
Row Name (Name) |
One key, meaningful within an expected table/domain | Inventory/save IDs when the table is implied. |
| Data Table Row Handle | Hard Data Table reference + row name | Designer-authored property that chooses a specific row. |
| Copied row struct | The current definition values | Short-lived calculation/presentation after successful lookup. Not canonical identity. |
Display Text |
Localizable player-facing words | UI only. Never reverse-map it to gameplay identity. |
| Enum | Closed compile-time set | A tiny set whose members rarely change; poor for large designer-authored catalogs. |
| Gameplay Tag | Hierarchical tag identity | Categories/capabilities and sometimes project IDs when tag governance already exists. |
| Primary Asset ID | Asset type + asset name managed by Asset Manager | Larger on-demand asset catalogs, cooking, and load/unload control. |
| Soft object path/reference | Indirect asset location | Deferred asset loading. It identifies an asset, not a row inside a table. |
Going deeper¶
- Get Data Table Row / Row Found / Row Not Found - resolve a row key and handle missing definitions.
- Data Tables vs Data Assets - choose the definition container before choosing its identity.
- Small Inventory Architecture - separate item definitions, runtime entries, world actors, and saved state.
- Save Game: Create / Save / Load Game to Slot - copy durable values into a SaveGame object and rebuild runtime state.
- Soft Object/Class References and async loading - defer table/asset loads without confusing a path with a resolved object.
- Official references: FDataTableRowHandle, FName, Data Driven Gameplay Elements, and Asset Redirectors.
- Engine source (requires engine access; we do not reproduce it):
FDataTableRowHandle::GetRow,IsNull, and its two stored members inEngine/Source/Runtime/Engine/Classes/Engine/DataTable.h;UDataTable::GetRowNamesand import handling inEngine/Source/Runtime/Engine/Private/DataTable.cpp; andFNameinEngine/Source/Runtime/Core/Public/UObject/NameTypes.h.