What is the difference between dynamic fields and dynamic object fields?
November 9, 2026
Both attach named data to an object's UID at runtime. The difference is what happens to the value. A dynamic field (sui::dynamic_field) can hold any value with store. If that value is an object, it gets wrapped and can no longer be found by its ID in explorers, wallets or RPC queries. A dynamic object field (sui::dynamic_object_field) only accepts objects (key + store) and keeps them as real objects, still visible at their own ID. That costs a bit more, because each access loads two objects instead of one.
Side by side
dynamic_field |
dynamic_object_field |
|
|---|---|---|
| Value constraint | store |
key + store |
| Can hold primitives and plain structs | Yes | No |
| Stored object visible at its own ID | No (wrapped) | Yes |
| Objects loaded per access | 1 | 2 (field plus child) |
| Get child's ID without its type | No | dof::id(&uid, name) |
| Collections built on it | Table, Bag, LinkedTable |
ObjectTable, ObjectBag |
The function names are the same in both modules: add, borrow, borrow_mut, remove, exists, exists_with_type, remove_opt and replace. They also abort with the same error codes.
How they're stored
A dynamic field is a single hidden Field<Name, Value> object owned by the parent. Its ID is derived from the parent ID and the name, and the value lives inside it.
A dynamic object field stores the name wrapped in an internal Wrapper<Name> type. The Field holds only the child's ID, and the child object is then owned by that field object. That's why the child keeps its identity, why reading it takes an extra object load, and why the Wrapper type means a dynamic_field and a dynamic_object_field with the same name on the same parent never collide.
Example
module example::character;
use std::string::String;
use sui::dynamic_field as df;
use sui::dynamic_object_field as dof;
public struct Character has key { id: UID }
public struct Hat has key, store { id: UID, color: String }
public struct Nickname has store, drop { value: String }
public fun equip_hat(c: &mut Character, hat: Hat) {
// Hat stays visible on explorers at its own ID.
dof::add(&mut c.id, b"hat", hat);
}
public fun set_nickname(c: &mut Character, value: String) {
// Plain data: dynamic_field is the only option and the cheaper one.
df::add(&mut c.id, b"nickname", Nickname { value });
}
public fun hat_id(c: &Character): Option<ID> {
dof::id(&c.id, b"hat")
}
public fun unequip_hat(c: &mut Character): Hat {
dof::remove(&mut c.id, b"hat")
}Which one to use
- Default to
dynamic_field. The Move Book recommends it unless you need to find the child by ID. It's cheaper and works for anystorevalue. - Use
dynamic_object_fieldwhen the child is an asset people should still see and look up, for example an NFT equipped on a character, or items in a kiosk-like container that wallets and explorers should show. - Either way, you can't pass the child directly as a transaction input. A transaction that tries to use a dynamic object field child by ID is rejected. You always go through the parent (
dof::borrow_mut(&mut parent.id, name)) or remove it first.
Offchain
In @mysten/sui v2, client.listDynamicFields({ parentId }) returns entries with $kind set to 'DynamicField' or 'DynamicObject'. Object entries carry a childId, and client.getDynamicObjectField({ parentId, name }) returns the child object itself. A dynamic object field child reports an ObjectOwner owner, which is the field object, not the parent directly. For the basics, see What are dynamic fields on Sui?.
Sources
- Dynamic Fields, Sui documentation
- Dynamic Object Fields, The Move Book
- dynamic_object_field.move, Sui framework source