Learn how to extend objects at runtime with dynamic fields and understand object wrapping patterns in Sui.
Dynamic fields allow you to add fields to objects after they’ve been created, enabling flexible data structures and efficient storage patterns. Unlike static fields declared in the struct definition, dynamic fields can have names determined at runtime.
/// In addition to the fields declared in its type definition, a Sui object can have dynamic fields/// that can be added after the object has been constructed. Unlike ordinary field names/// (which are always statically declared identifiers) a dynamic field name can be any value with/// the `copy`, `drop`, and `store` abilities, e.g. an integer, a boolean, or a string./// This gives Sui programmers the flexibility to extend objects on-the-fly, and it also serves as a/// building block for core collection typesmodule sui::dynamic_field;
Dynamic fields are stored as separate objects with a special structure:
/// Internal object used for storing the field and valuepublic struct Field<Name: copy + drop + store, Value: store> has key { /// Determined by the hash of the object ID, the field name value and it's type, /// i.e. hash(parent.id || name || Name) id: UID, /// The value for the name of this field name: Name, /// The value bound to this field value: Value,}
Each dynamic field is stored as a separate object whose ID is deterministically derived from the parent object’s ID, the field name, and its type.
/// Adds a dynamic field to the object `object: &mut UID` at field specified by `name: Name`./// Aborts with `EFieldAlreadyExists` if the object already has that field with that name.public fun add<Name: copy + drop + store, Value: store>( object: &mut UID, name: Name, value: Value,) { let object_addr = object.to_address(); let hash = hash_type_and_key(object_addr, name); assert!(!has_child_object(object_addr, hash), EFieldAlreadyExists); let field = Field { id: object::new_uid_from_hash(hash), name, value, }; add_child_object(object_addr, field)}
/// Immutably borrows the `object`s dynamic field with the name specified by `name: Name`./// Aborts with `EFieldDoesNotExist` if the object does not have a field with that name./// Aborts with `EFieldTypeMismatch` if the field exists, but the value does not have the specified type.public fun borrow<Name: copy + drop + store, Value: store>(object: &UID, name: Name): &Value { let object_addr = object.to_address(); let hash = hash_type_and_key(object_addr, name); let field = borrow_child_object<Field<Name, Value>>(object, hash); &field.value}
/// Mutably borrows the `object`s dynamic field with the name specified by `name: Name`./// Aborts with `EFieldDoesNotExist` if the object does not have a field with that name./// Aborts with `EFieldTypeMismatch` if the field exists, but the value does not have the specified type.public fun borrow_mut<Name: copy + drop + store, Value: store>( object: &mut UID, name: Name,): &mut Value { let object_addr = object.to_address(); let hash = hash_type_and_key(object_addr, name); let field = borrow_child_object_mut<Field<Name, Value>>(object, hash); &mut field.value}
/// Removes the `object`s dynamic field with the name specified by `name: Name` and returns the/// bound value./// Aborts with `EFieldDoesNotExist` if the object does not have a field with that name./// Aborts with `EFieldTypeMismatch` if the field exists, but the value does not have the specified type.public fun remove<Name: copy + drop + store, Value: store>(object: &mut UID, name: Name): Value { let object_addr = object.to_address(); let hash = hash_type_and_key(object_addr, name); let Field { id, name: _, value } = remove_child_object<Field<Name, Value>>(object_addr, hash); id.delete(); value}
/// Returns true if and only if the `object` has a dynamic field with the name specified by/// `name: Name` but without specifying the `Value` typepublic fun exists_<Name: copy + drop + store>(object: &UID, name: Name): bool { let object_addr = object.to_address(); let hash = hash_type_and_key(object_addr, name); has_child_object(object_addr, hash)}/// Returns true if and only if the `object` has a dynamic field with the name specified by/// `name: Name` with an assigned value of type `Value`.public fun exists_with_type<Name: copy + drop + store, Value: store>( object: &UID, name: Name,): bool { let object_addr = object.to_address(); let hash = hash_type_and_key(object_addr, name); has_child_object_with_ty<Field<Name, Value>>(object_addr, hash)}
module dynamic_fields::example;use sui::dynamic_object_field as ofield;public struct Parent has key { id: UID,}public struct Child has key, store { id: UID, count: u64,}public fun add_child(parent: &mut Parent, child: Child) { ofield::add(&mut parent.id, b"child", child);}/// If `child` is a dynamic field of some `Parent`, then this/// function cannot be called directly, because `child` must be/// accessed via its parent.public fun mutate_child(child: &mut Child) { child.count = child.count + 1;}public fun mutate_child_via_parent(parent: &mut Parent) { mutate_child(ofield::borrow_mut(&mut parent.id, b"child"))}public fun reclaim_child(parent: &mut Parent): Child { ofield::remove(&mut parent.id, b"child")}public fun delete_child(parent: &mut Parent) { let Child { id, count: _ } = reclaim_child(parent); object::delete(id);}
/// The object already has a dynamic field with this name (with the value and type specified)const EFieldAlreadyExists: u64 = 0;/// Cannot load dynamic field./// The object does not have a dynamic field with this name (with the value and type specified)const EFieldDoesNotExist: u64 = 1;/// The object has a field with that name, but the value type does not matchconst EFieldTypeMismatch: u64 = 2;/// Failed to serialize the field's nameconst EBCSSerializationFailure: u64 = 3;/// The object added as a dynamic field was previously a shared objectconst ESharedObjectOperationNotSupported: u64 = 4;
Always check field existence before adding to avoid EFieldAlreadyExists. Use exists_ or exists_with_type to verify.
/// Removes the dynamic field if it exists. Returns the `some(Value)` if it exists or none otherwise.public fun remove_if_exists<Name: copy + drop + store, Value: store>( object: &mut UID, name: Name,): Option<Value> { if (exists_<Name>(object, name)) { option::some(remove(object, name)) } else { option::none() }}
Instead of raw bytes, use custom types for field names:
public struct MetadataKey has copy, drop, store {}dynamic_field::add(&mut obj.id, MetadataKey {}, metadata);
Clean up unused fields
Dynamic fields cost storage. Remove them when no longer needed:
if (dynamic_field::exists_(&obj.id, key)) { let _: OldData = dynamic_field::remove(&mut obj.id, key);}
Handle deletion carefully
Remember that deleting a parent with dynamic fields will make those fields inaccessible:
#[test]/// This is not a desirable property, but objects can be deleted/// with dynamic fields still attached, and they become/// inaccessible.fun test_delete_with_child_attached() { let mut ts = test_scenario::begin(@0xA); let ctx = ts.ctx(); let mut p = Parent { id: object::new(ctx) }; p.add_child(Child { id: object::new(ctx), count: 0 }); let Parent { id } = p; id.delete(); ts.end();}