Structs and Resources
Learn about structs and resources in Move, which are used to define data structures.
Structs and Resources
Structs are custom data types that contain typed fields and form the foundation of Move's type system. They can hold any non-reference values, including other structs, and are essential for modeling both simple data and complex data structures.
Move structs have unique ownership semantics by default:
- Linear: Values cannot be copied - they must be explicitly moved or transferred
- Ephemeral: Values cannot be dropped - they must be consumed or destructured
- Private: Values cannot be stored in global storage without explicit abilities
When structs have these default properties, we call them resources - perfect for representing valuable assets like tokens, NFTs, or account balances where duplication or accidental loss would be problematic.
You can grant structs abilities (copy, drop, store, key) to relax these restrictions and enable different behaviors based on your use case.
Defining Structs
Structs are declared within modules using the struct keyword. Each struct defines a custom type with named, typed fields that can hold data:
module 0x2::game {
struct Player { level: u64, active: bool } // struct with fields
struct Team {} // empty struct
struct Match { player: Player, } // nested struct (trailing comma allowed)
}Key rules for struct definitions:
- Must be declared inside a module
- Can contain zero or more typed fields
- Fields can be primitive types, other structs, or generic types
- Trailing commas are permitted for better code formatting
Recursive Structs
Move prevents recursive struct definitions to ensure memory safety and avoid infinite data structures:
struct Node { next: Node } // ERROR: cyclic data
struct Tree { left: Tree } // ERROR: cyclic dataWhy this restriction exists:
- Prevents infinite memory allocation
- Ensures predictable struct sizes at compile time
- Maintains Move's safety guarantees
Struct Abilities
Abilities control what operations are permitted on struct values. By default, structs have no abilities, making them resources. You can grant specific abilities using the has keyword:
module 0x2::game {
struct Player has copy, drop { level: u64, active: bool } // copyable and droppable
struct Token has store { amount: u64 } // storable only
struct Account has key { balance: u64 } // global storage capable
struct Resource { value: u64 } // no abilities (pure resource)
}The four abilities:
copy: Enables value duplication withcopyoperatordrop: Allows automatic cleanup when values go out of scopestore: Permits storage inside other structs and global storagekey: Enables top-level global storage operations
Tip: Only grant abilities your struct actually needs. Resources (no abilities) are perfect for valuable assets.
Naming Conventions
Move enforces specific naming rules for structs to maintain consistency and reserve space for future language features:
Naming Rules:
- First character: Must be uppercase letter (A-Z)
- Subsequent characters: Letters (a-z, A-Z), digits (0-9), underscores (_)
- Case style: PascalCase is the recommended convention
// Valid struct names
struct Player {}
struct GameState {}
struct NFT_Metadata {}
struct Account2024 {}
// Invalid struct names
struct player {} // starts with lowercase
struct _Config {} // starts with underscore
struct 2Player {} // starts with digitBest Practices:
- Use descriptive, meaningful names
- Follow PascalCase convention
- Avoid abbreviations when clarity matters
- Consider the domain context (e.g.,
UserProfilevsProfile)
Working with Structs
Creating Struct Values
Struct values are created using struct literal syntax - specify the struct name followed by field values in braces:
module 0x2::game {
struct Player has drop { level: u64, active: bool }
struct Match has drop { player: Player, round: u64 }
fun create_game_data() {
// Basic struct creation
let player = Player { level: 1, active: true };
// Nested struct creation
let match = Match {
player: Player { level: 5, active: true },
round: 1
};
// Using existing values
let another_match = Match { player, round: 2 };
}
}Important notes:
- All fields must be provided (no default values)
- Field order doesn't matter in struct literals
- Values are moved into the struct (ownership transfer)
Field Name Punning
Field punning allows shorthand syntax when a local variable name matches the field name:
fun create_player_data(level: u64, active: bool) {
// Verbose syntax
let player1 = Player { level: level, active: active };
// Punning syntax (equivalent)
let player2 = Player { level, active };
// Mixed usage
let player3 = Player { level, active: true };
}When to use punning:
- SUCCESS: When variable names naturally match field names
- SUCCESS: In constructor functions with matching parameters
- ERROR: Avoid when it reduces code clarity
Destructuring Structs
Struct values are consumed (destroyed) through pattern matching, which extracts field values and transfers ownership:
Why destructuring matters:
- Only way to access fields of structs without abilities
- Ensures resources are properly handled
- Enables clean data extraction patterns
module 0x2::game {
struct Player { level: u64, active: bool }
struct Team { player: Player }
struct Tournament {}
// Basic destructuring with field renaming
fun extract_player_data() {
let player = Player { level: 5, active: true };
let Player { level, active: is_active } = player;
// ^ field punning ^ field renaming
// Creates two new variables:
// level: u64 = 5
// is_active: bool = true
}
// Partial destructuring with wildcards
fun get_player_level() {
let player = Player { level: 10, active: false };
let Player { level, active: _ } = player; // ignore 'active' field
// Only 'level' is bound, 'active' is discarded
}
// Assignment to existing variables
fun update_from_player() {
let level: u64;
let active: bool;
// Destructure directly into existing variables
Player { level, active } = Player { level: 8, active: true };
}
// Destructuring references (non-consuming)
fun read_player_data() {
let player = Player { level: 3, active: true };
let Player { level, active } = &player; // borrow, don't consume
// Creates references:
// level: &u64
// active: &bool
// 'player' is still available for use
}
// Destructuring mutable references
fun modify_player_data() {
let mut player = Player { level: 1, active: false };
let Player { level, active } = &mut player;
// Creates mutable references:
// level: &mut u64
// active: &mut bool
*level = 99;
*active = true;
}
// Nested destructuring
fun extract_nested_data() {
let team = Team { player: Player { level: 7, active: true } };
let Team { player: Player { level, active } } = team;
// ^ nested pattern matching
// Directly extracts from nested struct:
// level: u64 = 7
// active: bool = true
}
// Empty struct destructuring
fun handle_tournament() {
let tournament = Tournament {};
let Tournament {} = tournament; // must still destructure empty structs
}
}Borrowing and References
Borrowing creates references to structs and fields without transferring ownership. This is essential for reading data without consuming resources:
fun demonstrate_borrowing() {
let mut player = Player { level: 3, active: true };
// Borrow entire struct (immutable)
let player_ref: &Player = &player;
let current_level = player_ref.level; // read through reference
// Borrow specific field (immutable)
let level_ref: &u64 = &player.level;
let level_value = *level_ref; // dereference to get value
// Borrow field (mutable)
let level_mut: &mut u64 = &mut player.level;
*level_mut = 42; // modify through mutable reference
// player is still accessible after borrowing
let final_level = player.level; // final_level = 42
}Key borrowing concepts:
- Immutable borrow (
&): Read-only access, multiple borrows allowed - Mutable borrow (
&mut): Read-write access, exclusive borrow - Field borrowing: Can borrow individual fields directly
- Non-consuming: Original value remains available after borrowing
Advanced Borrowing Patterns
Nested field borrowing allows direct access to deeply nested data:
fun nested_borrowing_examples() {
let player = Player { level: 3, active: true };
let team = Team { player };
// Direct nested field access
let level_ref = &team.player.level;
let active_ref = &team.player.active;
// Mutable nested borrowing
let mut team2 = Team { player: Player { level: 1, active: false } };
let level_mut = &mut team2.player.level;
*level_mut = 50;
}Borrowing through references - you can chain reference operations:
fun chained_borrowing() {
let player = Player { level: 5, active: true };
let player_ref = &player;
// These are equivalent:
let level1 = &player.level; // direct field borrow
let level2 = &player_ref.level; // borrow through reference
// Both create &u64 references to the same field
}Borrowing rules:
- Can borrow nested fields arbitrarily deep
- Reference chains are automatically dereferenced
- Mutable borrows require mutable access at every level
Field Access Patterns
Reading Field Values
Move provides multiple ways to read field values depending on the struct's abilities and your needs:
fun reading_examples() {
let player = Player { level: 3, active: true };
let team = Team { player: copy player }; // requires 'copy' ability
// Method 1: Explicit borrow and dereference
let level: u64 = *&player.level;
let active: bool = *&player.active;
// Method 2: Copy entire struct (if it has 'copy' ability)
let player_copy: Player = *&team.player;
// Method 3: Borrow for temporary access
let level_ref = &player.level;
let level_value = *level_ref;
}When to use each method:
- Borrow + dereference (
*&): When you need the value but struct lackscopy - Direct copy: When struct has
copyability and you need ownership - Reference: When you only need temporary access
Implicit Field Access
For primitive types (integers, booleans, addresses), Move allows direct field access without explicit borrowing:
fun implicit_access_examples() {
let player = Player { level: 3, active: true };
// Implicit copying for primitive fields
let level = player.level; // automatically copies u64
let active = player.active; // automatically copies bool
// Works with nested primitive fields
let match = Match { player: Player { level: 5, active: true }, round: 1 };
let nested_level = match.player.level; // copies nested u64
let round_num = match.round; // copies u64
}What gets implicit copying:
- SUCCESS: Primitive types:
u8,u16,u32,u64,u128,u256,bool,address - ERROR: Complex types: structs, vectors, references
- ERROR: Types without
copyability
Chaining field access:
// Multiple levels of nesting work automatically
let deep_value = game.tournament.match.player.level;Explicit Copying Requirements
For complex types (structs, vectors), Move requires explicit syntax to make copying operations visible and intentional:
fun explicit_copying_examples() {
let player = Player { level: 3, active: true };
let team = Team { player };
// SUCCESS: Explicit copy syntax required for structs
let player_copy: Player = *&team.player;
// ERROR: This would fail - implicit copying not allowed
// let player_copy2: Player = team.player;
// SUCCESS: For vectors, also need explicit copying
let scores = vector[100, 200, 300];
let scores_copy = *&scores; // explicit copy required
}Why explicit copying is required:
- Performance awareness: Copying large structs/vectors can be expensive
- Intentional design: Forces developers to think about copy costs
- Code clarity: Makes copying operations visible to code reviewers
- Memory safety: Prevents accidental expensive operations
Alternatives to copying:
- Use references (
&) for read-only access - Use mutable references (
&mut) for modifications - Restructure code to avoid unnecessary copies
Modifying Fields
Field modification uses dot notation and requires mutable access to the containing struct:
fun field_modification_examples() {
let mut player = Player { level: 3, active: true };
// Direct field modification
player.level = 42;
player.active = !player.active;
// Nested field modification
let mut team = Team { player };
team.player.level = 52; // modify nested field
// Replace entire nested struct
team.player = Player { level: 100, active: true };
// Modification through mutable reference
let player_ref = &mut team.player;
player_ref.level = player_ref.level + 10;
}Field modification rules:
- Struct must be declared with
mutfor modifications - Can modify fields at any nesting level
- Can replace entire field values
- Works through mutable references (
&mut)
Common modification patterns:
// Increment/update patterns
player.level += 1;
player.active = check_player_status();
// Conditional updates
if (player.level < 10) {
player.level = player.level * 2;
};Module Privacy and Access Control
Move enforces strict module-level encapsulation for struct operations, ensuring data integrity and controlled access:
Private operations (module-only):
- Creating structs: Only the defining module can construct struct values
- Destructuring structs: Only the defining module can pattern match and extract fields
- Field access: Only the defining module can directly read/write fields
Public operations (cross-module):
- Type usage: Other modules can use the struct type in function signatures
- Value passing: Struct values can be passed between modules
- Reference creation: Can create references to structs from other modules
module 0x2::bank {
struct Account { balance: u64 } // private fields
// Public constructor
public fun create_account(initial_balance: u64): Account {
Account { balance: initial_balance } // SUCCESS: allowed in defining module
}
// Public accessor
public fun get_balance(account: &Account): u64 {
account.balance // SUCCESS: field access allowed in defining module
}
}
module 0x2::user {
use 0x2::bank;
fun use_account() {
let account = bank::create_account(100); // SUCCESS: use public constructor
let balance = bank::get_balance(&account); // SUCCESS: use public accessor
// ERROR: These would fail:
// let Account { balance } = account; // can't destructure
// let direct_balance = account.balance; // can't access fields
}
}Cross-Module Usage Patterns
While struct internals are private, the type itself is public and can be used across modules:
// game.move
module 0x2::game {
struct Player has drop { level: u64 }
public fun new_player(): Player {
Player { level: 1 }
}
}// tournament.move
module 0x2::tournament {
use 0x2::game;
struct Roster has drop {
player: game::Player
}
fun f1(player: game::Player) {
let level = player.level;
// ^ error! cannot access fields of `player` here
}
fun f2() {
let roster = Roster { player: game::new_player() };
}
}Note that structs do not have visibility modifiers (e.g., public or private).
Resource Ownership Model
Move's ownership system ensures safe handling of valuable digital assets through strict compile-time checks:
Default struct behavior (resources):
- Cannot be copied: Prevents accidental duplication of valuable assets
- Cannot be dropped: Prevents accidental loss or destruction
- Must be consumed: All values must be explicitly handled
This model is perfect for representing digital assets like tokens, NFTs, or account balances where duplication or loss would be catastrophic.
module 0x2::inventory {
struct Item { rarity: u64 }
public fun copying_resource() {
let item = Item { rarity: 100 };
let item_copy = copy item; // error! 'copy'-ing requires the 'copy' ability
let item_ref = &item;
let another_copy = *item_ref // error! dereference requires the 'copy' ability
}
public fun destroying_resource1() {
let item = Item { rarity: 100 };
// error! when the function returns, item still contains a value.
// This destruction requires the 'drop' ability
}
public fun destroying_resource2(i: &mut Item) {
*i = Item { rarity: 100 } // error!
// destroying the old value via a write requires the 'drop' ability
}
}Manual Resource Destruction
To fix the second example (fun destroying_resource1), you would need to manually "unpack" the resource:
module 0x2::inventory {
struct Item { rarity: u64 }
public fun destroying_resource1_fixed() {
let item = Item { rarity: 100 };
let Item { rarity: _ } = item;
}
}Recall that you are only able to deconstruct a resource within the module in which it is defined. This can be leveraged to enforce certain invariants in a system, for example, conservation of money.
Adding Copy and Drop Abilities
If on the other hand, your struct does not represent something valuable, you can add the abilities copy and drop to get a struct value that might feel more familiar from other programming languages:
module 0x2::collectible {
struct Card has copy, drop { power: u64 }
public fun run() {
let card = Card { power: 100 };
let card_copy = copy card;
// ^ this code copies card, whereas `let x = card` or
// `let x = move card` both move card
let power = card.power; // power = 100
let power_copy = card_copy.power; // power_copy = 100
// both card and card_copy are implicitly discarded when the function returns
}
}Storing Resources in Global Storage
Only structs with the key ability can be saved directly in persistent global storage. All values stored within those key structs must have the store ability. See the ability and global storage chapters for more detail.
Examples
Here are two short examples of how you might use structs to represent valuable data (in the case of Coin) or more classical data (in the case of Point and Circle).
Example 1: Token
module 0x2::token {
// We do not want the Token to be copied because that would be duplicating this "asset",
// so we do not give the struct the 'copy' ability.
// Similarly, we do not want programmers to destroy tokens, so we do not give the struct the
// 'drop' ability.
// However, we *want* users of the modules to be able to store this token in persistent global
// storage, so we grant the struct the 'store' ability. This struct will only be inside of
// other resources inside of global storage, so we do not give the struct the 'key' ability.
struct Token has store {
amount: u64,
}
public fun create(amount: u64): Token {
// You would want to gate this function with some form of access control to prevent
// anyone using this module from creating an infinite amount of tokens.
Token { amount }
}
public fun extract(token: &mut Token, amount: u64): Token {
assert!(token.amount >= amount, 1000);
token.amount = token.amount - amount;
Token { amount }
}
public fun combine(token: &mut Token, other: Token) {
let Token { amount } = other;
token.amount = token.amount + amount;
}
public fun divide(token: Token, amount: u64): (Token, Token) {
let other = extract(&mut token, amount);
(token, other)
}
public fun join(token1: Token, token2: Token): Token {
combine(&mut token1, token2);
token1
}
public fun burn_empty(token: Token) {
let Token { amount } = token;
assert!(amount == 0, 1001);
}
}Example 2: Coordinates
module 0x2::location {
struct Position has copy, drop, store {
latitude: u64,
longitude: u64,
}
public fun create(latitude: u64, longitude: u64): Position {
Position {
latitude, longitude
}
}
public fun get_latitude(pos: &Position): u64 {
pos.latitude
}
public fun get_longitude(pos: &Position): u64 {
pos.longitude
}
fun abs_difference(a: u64, b: u64): u64 {
if (a < b) {
b - a
}
else {
a - b
}
}
public fun distance_squared(pos1: &Position, pos2: &Position): u64 {
let lat_diff = abs_difference(pos1.latitude, pos2.latitude);
let lon_diff = abs_difference(pos1.longitude, pos2.longitude);
lat_diff*lat_diff + lon_diff*lon_diff
}
}module 0x2::region {
use 0x2::location::{Self, Position};
struct Area has copy, drop, store {
center: Position,
range: u64,
}
public fun create(center: Position, range: u64): Area {
Area { center, range }
}
public fun overlaps(area1: &Area, area2: &Area): bool {
let distance = location::distance_squared(&area1.center, &area2.center);
let r1 = area1.range;
let r2 = area2.range;
distance*distance <= r1*r1 + 2*r1*r2 + r2*r2
}
}Summary
Structs are user-defined data types that enable safe resource modeling in Move:
- Definition: Custom data structures with typed fields, defined within modules
- Resources: Linear values that cannot be copied or dropped by default - perfect for valuable assets
- Abilities: Control struct behavior with
copy,drop,store, andkeyabilities - Privacy: Struct operations are module-private; public APIs required for external access
- Patterns: Use appropriate abilities for your use case (value types vs resources vs global storage)
Structs provide the foundation for safe, ownership-based programming in Move.