Skip to main content
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.

What are Dynamic Fields?

From the framework documentation:
/// 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 types
module sui::dynamic_field;

Dynamic Field Structure

Dynamic fields are stored as separate objects with a special structure:
/// Internal object used for storing the field and value
public 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.

Basic Operations

Adding Dynamic Fields

/// 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)
}

Reading Dynamic Fields

/// 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
}

Removing Dynamic Fields

/// 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
}

Checking Field Existence

/// Returns true if and only if the `object` has a dynamic field with the name specified by
/// `name: Name` but without specifying the `Value` type
public 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)
}

Practical Example

Here’s a complete example from the Sui examples:
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);
}

Dynamic Object Fields

Sui provides a variant specifically for storing objects as dynamic fields:
use sui::dynamic_object_field as ofield;

public fun add_child(parent: &mut Parent, child: Child) {
    ofield::add(&mut parent.id, b"child", child);
}
  • dynamic_field: For storing any value with store ability
  • dynamic_object_field: Specifically for storing objects (structs with key)
Dynamic object fields allow the child object to be accessed independently in some cases and provide better type safety for object storage.

Use Cases

Collections

Build hash maps, sets, and other collections

Extensibility

Add optional metadata without modifying the base struct

Efficient Storage

Store large amounts of data without hitting object size limits

Heterogeneous Data

Store different types in a single collection

Building a Table

Dynamic fields are the foundation for Sui’s Table type:
use sui::table::{Self, Table};

public struct GameState has key {
    id: UID,
    players: Table<address, PlayerData>,
}
The Table type uses dynamic fields internally to store key-value pairs efficiently.

Object Wrapping

Object wrapping is a different pattern where objects are stored directly in struct fields:
public struct Wrapper has key {
    id: UID,
    o: Object,
}

public fun wrap(o: Object, ctx: &mut TxContext) {
    transfer::transfer(Wrapper { id: object::new(ctx), o }, ctx.sender());
}

public fun unwrap(w: Wrapper, ctx: &TxContext) {
    let Wrapper { id, o } = w;
    id.delete();
    transfer::public_transfer(o, ctx.sender());
}

Wrapping vs Dynamic Fields

Advantages:
  • Simple and direct
  • Type-safe at compile time
  • Lower gas cost for small objects
Disadvantages:
  • Object becomes inaccessible when wrapped
  • Requires unwrapping to access
  • Fixed at compile time
Best for:
  • Temporarily hiding objects
  • Composition patterns
  • Fixed relationships

Error Handling

/// 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 match
const EFieldTypeMismatch: u64 = 2;
/// Failed to serialize the field's name
const EBCSSerializationFailure: u64 = 3;
/// The object added as a dynamic field was previously a shared object
const ESharedObjectOperationNotSupported: u64 = 4;
Always check field existence before adding to avoid EFieldAlreadyExists. Use exists_ or exists_with_type to verify.

Advanced: Optional Removal

/// 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()
    }
}

Testing Dynamic Fields

From the examples:
#[test]
fun test_add_reclaim() {
    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 });

    p.mutate_child_via_parent();

    let mut c = p.reclaim_child();
    assert!(c.count == 1, 0);

    c.mutate_child();
    assert!(c.count == 2, 1);

    let Child { id, count: _ } = c;
    id.delete();

    let Parent { id } = p;
    id.delete();

    ts.end();
}

Best Practices

  • Direct fields: For required, fixed data
  • Wrapping: For temporary encapsulation
  • Dynamic fields: For optional, extensible data
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);
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);
}
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();
}

Objects

Understand Sui’s object model

Object Storage

Learn how objects are stored

Collections

Use built-in collection types

Build docs developers (and LLMs) love