Skip to main content
The #[contracttype] macro allows you to define custom data structures that can be used in contract functions, stored in contract storage, and serialized to/from XDR.

Why Use Custom Types?

Custom types help you:
  • Structure data: Group related fields together
  • Improve type safety: Prevent mixing up function parameters
  • Enhance readability: Make contract interfaces self-documenting
  • Enable storage: Store complex data in contract storage

Defining Custom Types

Use the #[contracttype] macro on structs and enums:

Struct Types

use soroban_sdk::{contracttype, Address, Vec};

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct User {
    pub address: Address,
    pub balance: i128,
    pub is_active: bool,
}
It’s recommended to derive Clone, Debug, Eq, and PartialEq for convenience.

Enum Types

Enums can be simple discriminants or hold data:
#[contracttype]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Status {
    Pending = 0,
    Active = 1,
    Completed = 2,
    Cancelled = 3,
}
Enums with associated data:
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OrderStatus {
    Pending,
    Processing(Address),      // Contains processor address
    Shipped(String),          // Contains tracking number
    Delivered,
}

Complete Example

Here’s a contract using multiple custom types:
#![no_std]
use soroban_sdk::{
    contract, contractimpl, contracttype, Address, Env, String, Vec
};

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Product {
    pub id: u64,
    pub name: String,
    pub price: i128,
    pub category: Category,
}

#[contracttype]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Category {
    Electronics = 0,
    Clothing = 1,
    Food = 2,
    Books = 3,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Order {
    pub id: u64,
    pub buyer: Address,
    pub products: Vec<Product>,
    pub total: i128,
    pub status: OrderStatus,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OrderStatus {
    Created,
    Paid,
    Shipped,
    Delivered,
    Cancelled,
}

#[contract]
pub struct Contract;

#[contractimpl]
impl Contract {
    pub fn create_order(
        env: Env,
        id: u64,
        buyer: Address,
        products: Vec<Product>,
    ) -> Order {
        let total = products.iter()
            .map(|p| p.price)
            .sum();

        Order {
            id,
            buyer,
            products,
            total,
            status: OrderStatus::Created,
        }
    }

    pub fn get_product_category(product: Product) -> Category {
        product.category
    }
}

Nested Types

Custom types can contain other custom types:
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OrderItem {
    pub product_id: u64,
    pub quantity: u32,
    pub price: i128,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Order {
    pub id: u64,
    pub buyer: Address,
    pub items: Vec<OrderItem>,  // Contains another custom type
    pub total_price: i128,
}

Using Custom Types with Storage

Store custom types in contract storage:
#[contracttype]
pub enum DataKey {
    User(Address),
    Order(u64),
}

#[contractimpl]
impl Contract {
    pub fn save_user(env: Env, user: User) {
        env.storage()
            .persistent()
            .set(&DataKey::User(user.address.clone()), &user);
    }

    pub fn get_user(env: Env, address: Address) -> Option<User> {
        env.storage()
            .persistent()
            .get(&DataKey::User(address))
    }
}

Tuple Structs

You can define tuple structs for simple wrappers:
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Point(pub i64, pub i64);

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Coordinates(pub i64, pub Vec<i64>);

#[contractimpl]
impl Contract {
    pub fn distance(p1: Point, p2: Point) -> i64 {
        ((p2.0 - p1.0).pow(2) + (p2.1 - p1.1).pow(2)).sqrt()
    }
}

Enums with Complex Data

Enums can contain different types of data in each variant:
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PaymentMethod {
    Token(Address),           // Token contract address
    Native,                   // Native token (XLM)
    MultiToken(Vec<Address>), // Multiple tokens
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Result {
    Success(i128),           // Success with value
    Error(u32),              // Error with code
}

Pattern Matching

Use pattern matching with custom enums:
#[contractimpl]
impl Contract {
    pub fn calculate_price(item: Item) -> i128 {
        match item.category {
            Category::Electronics => item.base_price * 120 / 100,
            Category::Clothing => item.base_price * 110 / 100,
            Category::Food => item.base_price * 105 / 100,
            Category::Books => item.base_price,
        }
    }

    pub fn process_order(env: Env, order: Order) {
        match order.status {
            OrderStatus::Created => {
                // Handle new order
            },
            OrderStatus::Paid => {
                // Begin processing
            },
            OrderStatus::Shipped => {
                // Track shipment
            },
            OrderStatus::Delivered => {
                // Complete order
            },
            OrderStatus::Cancelled => {
                // Refund
            },
        }
    }
}

Testing Custom Types

Custom types work seamlessly in tests:
#[cfg(test)]
mod test {
    use super::*;
    use soroban_sdk::{Env, testutils::Address as _};

    #[test]
    fn test_custom_types() {
        let env = Env::default();
        let contract_id = env.register(Contract, ());
        let client = ContractClient::new(&env, &contract_id);

        let user = User {
            address: Address::generate(&env),
            balance: 1000,
            is_active: true,
        };

        client.save_user(&user);
        
        let retrieved = client.get_user(&user.address);
        assert_eq!(retrieved, Some(user));
    }

    #[test]
    fn test_enum_variants() {
        let env = Env::default();
        
        let status1 = OrderStatus::Created;
        let status2 = OrderStatus::Shipped;
        
        assert_ne!(status1, status2);
    }
}

XDR Serialization

Custom types are automatically serialized to/from XDR:
#[cfg(test)]
mod test {
    use super::*;
    use soroban_sdk::{Env, xdr::ScVal, TryFromVal};

    #[test]
    fn test_serialization() {
        let env = Env::default();
        
        let user = User {
            address: Address::generate(&env),
            balance: 1000,
            is_active: true,
        };

        // Convert to XDR
        let xdr_val: ScVal = user.clone().try_into().unwrap();
        
        // Convert back from XDR
        let roundtrip = User::try_from_val(&env, &xdr_val).unwrap();
        
        assert_eq!(user, roundtrip);
    }
}

Storage Keys

Use enums as storage keys for type-safe storage access:
#[contracttype]
pub enum DataKey {
    // Simple variants
    Admin,
    TotalSupply,
    
    // Variants with data
    Balance(Address),
    Allowance(Address, Address),
    Order(u64),
    User(Address),
}

#[contractimpl]
impl Contract {
    pub fn set_balance(env: Env, addr: Address, amount: i128) {
        env.storage()
            .persistent()
            .set(&DataKey::Balance(addr), &amount);
    }

    pub fn get_balance(env: Env, addr: Address) -> i128 {
        env.storage()
            .persistent()
            .get(&DataKey::Balance(addr))
            .unwrap_or(0)
    }
}
Using typed enum variants for storage keys prevents key collisions and makes code more maintainable.

Field Visibility

Fields can be public or private:
#[contracttype]
#[derive(Clone)]
pub struct User {
    pub address: Address,    // Public field
    balance: i128,           // Private field
}

impl User {
    // Provide controlled access to private fields
    pub fn balance(&self) -> i128 {
        self.balance
    }
}

Explicit Discriminants

Set explicit values for enum discriminants:
#[contracttype]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ErrorCode {
    InvalidInput = 1,
    Unauthorized = 2,
    InsufficientBalance = 3,
    NotFound = 4,
}
Once deployed, don’t change enum discriminant values as this will break existing stored data.

Limitations

Custom types have some restrictions:
  • Cannot implement arbitrary traits (only SDK-provided traits)
  • Cannot have generic type parameters
  • Cannot have lifetime parameters
  • Must be serializable to XDR

Best Practices

Choose clear, self-documenting names for types and fields:
pub struct Order { }  // Good
pub struct Ord { }    // Bad: unclear abbreviation
Each type should represent a single concept or entity.
Always derive Clone, Debug, Eq, PartialEq for custom types.
Represent different states with enum variants rather than boolean flags.
Consider how types might need to change in future versions.

Next Steps

Events

Use custom types in events

Error Handling

Define custom error types