Docs

ListDynamicFields - Query Object Dynamic Fields

Retrieve dynamic fields attached to Sui objects via gRPC with pagination support. Essential for exploring extensible objects and collections with Dwellir.

Query Dynamic Fields on Objects

The ListDynamicFields method retrieves all dynamic fields attached to a specific Sui object. Dynamic fields allow objects to be extended with additional data at runtime, commonly used for collections, key-value stores, and flexible data structures. This method supports pagination for objects with many fields.

Overview

Sui's object-centric model allows objects to carry dynamic fields--additional key-value pairs that can be added, modified, or removed at runtime without changing the object's Move struct definition. This mechanism underpins Sui's collection types (Table, Bag, ObjectTable, ObjectBag) and is commonly used for on-chain registries, user profiles, game inventories, and any pattern requiring flexible storage.

There are two types of dynamic fields on Sui: dynamic fields (where the value is stored inline) and dynamic object fields (where the value is a standalone object that can be accessed independently). The ListDynamicFields response includes both types, with the kind field distinguishing between them. Understanding this distinction is important because dynamic object fields can be transferred or accessed directly by their object ID, while regular dynamic fields can only be accessed through the parent object.

Key Capabilities

  • Collection Browsing: Enumerate all entries in Table, Bag, and custom collection types
  • Pagination Support: Handle objects with thousands of dynamic fields through page-based iteration
  • Field Type Inspection: Discover the key type and value type of each dynamic field
  • Object Discovery: Find child object IDs stored in dynamic object fields
  • Change Monitoring: Track additions, removals, and modifications to an object's dynamic fields over time

Method Signature

Service: sui.rpc.v2.StateService Method: ListDynamicFields Type: Unary RPC

Use Cases

Fetch All Dynamic Fields with Pagination

TypeScript
async function fetchAllDynamicFields(objectId: string): Promise<any[]> {
  const allFields: any[] = [];
  let pageToken: string | undefined = undefined;

  do {
    const response = await listDynamicFields(objectId, 50, pageToken);

    allFields.push(...response.fields);
    pageToken = response.next_page_token;

    console.log(`Fetched ${response.fields.length} fields, total: ${allFields.length}`);

  } while (pageToken);

  return allFields;
}

// Usage
const allFields = await fetchAllDynamicFields(objectId);
console.log(`Total fields: ${allFields.length}`);

Explore Collection/Map Structure

TypeScript
interface DynamicFieldInfo {
  name: string;
  type: string;
  valueObjectId: string;
  valueType: string;
}

async function exploreCollection(objectId: string): Promise<DynamicFieldInfo[]> {
  const fields = await fetchAllDynamicFields(objectId);

  return fields.map(field => ({
    name: field.name,
    type: field.type,
    valueObjectId: field.object_id,
    valueType: field.object_type
  }));
}

// Usage for exploring a Map or Table
const collectionFields = await exploreCollection(tableObjectId);

console.log('Collection contains:');
collectionFields.forEach(field => {
  console.log(`  ${field.name}: ${field.valueType}`);
});

Fetch Field Values

TypeScript
async function getDynamicFieldValue(
  parentObjectId: string,
  fieldName: string
): Promise<any | null> {
  const fields = await fetchAllDynamicFields(parentObjectId);

  const field = fields.find(f => f.name === fieldName);

  if (!field) {
    console.warn(`Field "${fieldName}" not found`);
    return null;
  }

  // Fetch the value object
  const valueObject = await getObject(field.object_id);

  return valueObject;
}

// Usage
const userProfile = await getDynamicFieldValue(
  registryObjectId,
  'user_123'
);

console.log('User profile:', userProfile);

Monitor Field Changes

TypeScript
class DynamicFieldMonitor {
  private previousFields = new Map<string, Set<string>>();

  async checkForChanges(objectId: string): Promise<{
    added: string[];
    removed: string[];
    modified: string[];
  }> {
    const currentFields = await fetchAllDynamicFields(objectId);
    const currentSet = new Set(
      currentFields.map(f => `${f.name}:${f.version}`)
    );

    const previousSet = this.previousFields.get(objectId) || new Set();

    const added: string[] = [];
    const removed: string[] = [];
    const modified: string[] = [];

    // Check for added and modified fields
    for (const field of currentFields) {
      const key = `${field.name}:${field.version}`;
      const prevKey = Array.from(previousSet).find(k => k.startsWith(`${field.name}:`));

      if (!prevKey) {
        added.push(field.name);
      } else if (prevKey !== key) {
        modified.push(field.name);
      }
    }

    // Check for removed fields
    const currentNames = new Set(currentFields.map(f => f.name));
    for (const prev of previousSet) {
      const name = prev.split(':')[0];
      if (!currentNames.has(name)) {
        removed.push(name);
      }
    }

    this.previousFields.set(objectId, currentSet);

    return { added, removed, modified };
  }
}

// Usage
const monitor = new DynamicFieldMonitor();

setInterval(async () => {
  const changes = await monitor.checkForChanges(objectId);

  if (changes.added.length > 0) {
    console.log('Fields added:', changes.added);
  }
  if (changes.removed.length > 0) {
    console.log('Fields removed:', changes.removed);
  }
  if (changes.modified.length > 0) {
    console.log('Fields modified:', changes.modified);
  }
}, 10000);

Build Field Index

TypeScript
interface FieldIndex {
  [fieldName: string]: {
    objectId: string;
    type: string;
    version: number;
  };
}

async function buildFieldIndex(objectId: string): Promise<FieldIndex> {
  const fields = await fetchAllDynamicFields(objectId);

  const index: FieldIndex = {};

  for (const field of fields) {
    index[field.name] = {
      objectId: field.object_id,
      type: field.object_type,
      version: parseInt(field.version)
    };
  }

  return index;
}

// Usage
const index = await buildFieldIndex(objectId);

// Quick lookups
const userField = index['user_123'];
if (userField) {
  console.log(`User object: ${userField.objectId}`);
}

Search Fields by Pattern

TypeScript
async function searchFields(
  objectId: string,
  pattern: RegExp
): Promise<any[]> {
  const allFields = await fetchAllDynamicFields(objectId);

  return allFields.filter(field => pattern.test(field.name));
}

// Usage
const userFields = await searchFields(
  registryObjectId,
  /^user_\d+$/
);

console.log(`Found ${userFields.length} user fields`);

Best Practices

Efficient Pagination

TypeScript
async function fetchFieldsPage(
  objectId: string,
  pageSize: number = 50
): Promise<AsyncGenerator<any[], void, unknown>> {
  return async function* () {
    let pageToken: string | undefined = undefined;

    do {
      const response = await listDynamicFields(objectId, pageSize, pageToken);

      yield response.fields;

      pageToken = response.next_page_token;
    } while (pageToken);
  }();
}

// Usage with async iteration
const pages = await fetchFieldsPage(objectId);

for await (const page of pages) {
  console.log(`Processing ${page.length} fields`);
  // Process each page
}

Cache Field Listings

TypeScript
class DynamicFieldCache {
  private cache = new Map<string, { data: any[]; timestamp: number }>();
  private ttl = 30000; // 30 seconds

  async getFields(objectId: string): Promise<any[]> {
    const cached = this.cache.get(objectId);

    if (cached && Date.now() - cached.timestamp < this.ttl) {
      return cached.data;
    }

    const fields = await fetchAllDynamicFields(objectId);

    this.cache.set(objectId, {
      data: fields,
      timestamp: Date.now()
    });

    return fields;
  }

  invalidate(objectId: string): void {
    this.cache.delete(objectId);
  }
}

Handle Empty Results

TypeScript
async function getDynamicFieldsSafe(objectId: string): Promise<any[]> {
  try {
    const fields = await fetchAllDynamicFields(objectId);

    if (fields.length === 0) {
      console.log(`Object ${objectId} has no dynamic fields`);
    }

    return fields;
  } catch (error: any) {
    if (error.code === grpc.status.NOT_FOUND) {
      console.warn(`Object ${objectId} not found`);
      return [];
    }

    throw error;
  }
}

Performance Characteristics

MetricValue
Typical Latency20-60ms (depends on field count)
Page Size50 recommended, 100 max
Response Size1-5KB per page
Cache RecommendedYes (30s TTL)

Common Errors

Error CodeScenarioSolution
NOT_FOUNDObject doesn't existVerify object ID
INVALID_ARGUMENTInvalid object ID formatCheck ID format
OUT_OF_RANGELimit exceeds maximumUse limit ≤ 100
INVALID_ARGUMENTInvalid page tokenUse token from previous response

Dynamic Fields vs Static Fields

AspectDynamic FieldsStatic Fields
Definition TimeRuntimeCompile time
FlexibilityHighLow
Query MethodListDynamicFieldsGetObject
Use CaseCollections, mapsFixed structure
Gas CostHigherLower

Need help? Contact support@dwellir.com or check the gRPC overview.