Section 01 · What
What a struct is in Rust
A named collection of fields with explicit types — the fundamental building block for on chain state in every Solana program.
Quick answer
What is a Rust struct in the context of Solana? A Rust struct in Solana defines the shape of data stored inside an account. When you mark a struct with #[account], Anchor treats it as an on chain account type that can be initialised, read, and mutated through instructions. The fields describe exactly what state your program tracks.
A struct is a named collection of fields, each with an explicit type. You declare one with the struct keyword followed by a name and a set of named fields inside curly braces. Unlike an enum, a struct has no variants — it always has the same fields, every instance.
Rust structs are value types by default. They live on the stack unless you put them behind a pointer. This differs from Java or Python classes, which are always heap allocated references. In Solana, your account structs end up serialised to the heap allocated data field of the account, but within your instruction logic they are stack values you can move and copy normally.
The key distinction from enums: a struct says "this thing has all of these fields". An enum says "this thing is one of these variants". Account state is almost always modelled with structs because an account's data layout is fixed at creation time — you cannot change it to a different variant without reallocating.
use anchor_lang::prelude::*;
// The simplest possible account struct
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub authority: Pubkey, // 32 bytes
pub count: u64, // 8 bytes
pub bump: u8, // 1 byte (PDA bump seed)
}
// Space: 8 (discriminator) + 32 + 8 + 1 = 49 bytesSection 02 · Traits
Derive macros and traits every Solana struct needs
Five derive macros turn a plain Rust struct into a fully wired Anchor account type. Each one earns its place.
Debug and Clone
Debug lets you print struct values with {:?} — essential for test logs and error messages. Clone provides an explicit .clone() method for deep copies. Derive both on every struct. Anchor's #[account] macro derives Clone automatically; you still need to add Debug manually if you want it.
AnchorSerialize and AnchorDeserialize
These are Anchor's wrappers around Borsh, the Binary Object Representation Serializer for Hashing. Borsh produces deterministic binary representations — the same struct always serialises to the same bytes regardless of platform. This determinism is what Solana needs for cross-validator consistency. The #[account] macro derives both automatically.
InitSpace
InitSpace calculates the account's required data size at compile time and exposes it as a INIT_SPACE constant. Use it in your init constraint: space = 8 + Counter::INIT_SPACE. Without it you calculate size manually, which drifts out of sync as your struct evolves. For String and Vec fields, add #[max_len(N)] so the macro knows the upper bound.
Section 03 · Account Struct
Defining account state with a struct
The #[account] macro is one line. What it generates is the foundation that makes Anchor's account validation safe.
Place #[account] above a struct declaration and Anchor generates the discriminator, wires up serialisation, and marks the struct as owned by your program. The macro is the single most important line in your account definition.
The discriminator is the most important detail the macro generates. Anchor computes it as the first 8 bytes of sha256("account:StructName"). Every time a user passes an account to your instruction, Anchor reads the first 8 bytes and checks them against the expected discriminator for that type. If they do not match, Anchor returns an error before your instruction code runs. This single check prevents dozens of account substitution exploits.
The discriminator is a security boundary
Without a discriminator, an attacker could pass any account with the right size to your instruction. Anchor's discriminator check means an account can only be used where the type matches. Never skip it by writing raw Borsh account handling — the #[account] macro is there precisely because manual account validation is where most exploits originate.
Section 04 · Borsh
Borsh serialization: how structs become bytes
Every field maps to a predictable number of bytes. Knowing the mapping is how you calculate account space without guessing.
Borsh writes fields in declaration order with no padding between them. Fixed size types have exact, predictable sizes. u8 is 1 byte. u16 is 2 bytes. u32 is 4 bytes. u64 is 8 bytes. u128 is 16 bytes. The signed variants (i8, i16, i32, i64, i128) are the same sizes. bool is 1 byte. Pubkey is always 32 bytes.
Variable size types add a 4 byte little endian length prefix before the data. A String with value “hello” takes 9 bytes: 4 for the length, 5 for the UTF-8 characters. A Vec of Pubkey with 3 entries takes 4 + (3 × 32) = 100 bytes. An Option of T takes 1 byte for the discriminant (0 for None, 1 for Some) plus the size of T when the value is Some.
// Manual space calculation (equivalent to InitSpace)
impl Post {
pub const INIT_SPACE: usize =
8 // Anchor discriminator
+ 32 // author (Pubkey)
+ 8 // timestamp (i64)
+ 8 // like_count (u64)
+ 1 // is_published (bool)
+ 4 + 100 // title (String: 4-byte length + 100 chars max)
+ 4 + 2000 // content (String: 4-byte length + 2000 chars max)
+ 4 + 5*32; // tags (Vec<Pubkey>: 4-byte len + 5 × 32 bytes)
}Section 05 · InitSpace
Calculating account space with InitSpace
Manual size constants drift as structs evolve. InitSpace computes the sum at compile time so the code always reflects the actual struct.
Derive InitSpace alongside #[account]. For string and vec fields, add #[max_len(N)] to tell the macro the upper bound. The macro then exposes YourStruct::INIT_SPACE as a usize constant. Your init instruction then uses space = 8 + YourStruct::INIT_SPACE.
// Account struct with strings and vecs — requires max_len
#[account]
#[derive(InitSpace)]
pub struct Post {
pub author: Pubkey, // 32 bytes
pub timestamp: i64, // 8 bytes
pub like_count: u64, // 8 bytes
pub is_published: bool, // 1 byte
#[max_len(100)]
pub title: String, // 4 + 100 = 104 bytes
#[max_len(2000)]
pub content: String, // 4 + 2000 = 2004 bytes
#[max_len(5, 32)]
pub tags: Vec<Pubkey>, // 4 + (5 × 32) = 164 bytes
}
// Space: 8 + 32 + 8 + 8 + 1 + 104 + 2004 + 164 = 2329 bytesThe numbers add up fast. That post struct at 2329 bytes costs roughly 0.004 SOL in rent exemption at current rates. Choose maximum lengths conservatively — overprovision for fields that will actually grow, and keep fields that stay small exactly as small as they need to be.
For the full breakdown of how each field type contributes to the total, the Rust Data Types for Solana guide covers every type with size tables and use cases.
Section 06 · Nested
Nested structs inside account data
Group related fields into sub-structs that live inline inside a parent account. The parent's space calculation includes them automatically.
Not every struct in your program is a top level account type. Sometimes you want to group related fields into a sub-struct that lives inline inside a parent account. A metadata block inside a program configuration account is a common example.
The rule is straightforward: nested structs derive AnchorSerialize, AnchorDeserialize, Clone, and InitSpace, but do NOT carry #[account]. The #[account] macro marks a struct as a standalone on chain account with its own discriminator. A nested struct is just an inlined set of bytes inside the parent — it has no discriminator of its own.
// Nested struct: inner type does NOT get #[account]
#[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace)]
pub struct Metadata {
pub version: u8, // 1 byte
#[max_len(50)]
pub name: String, // 4 + 50 = 54 bytes
}
// Metadata total = 1 + 54 = 55 bytes
#[account]
#[derive(InitSpace)]
pub struct Program {
pub owner: Pubkey, // 32 bytes
pub meta: Metadata, // 55 bytes (inline)
pub stake: u64, // 8 bytes
}
// Total: 8 + 32 + 55 + 8 = 103 bytesWhen Anchor serialises the parent, it calls the nested struct’s AnchorSerialize implementation inline, writing its bytes directly into the parent’s byte stream. The parent’s space calculation includes the nested struct’s size automatically via InitSpace.
For the account constraints that control how structs are initialised and mutated in instructions, the Anchor Accounts Deep Dive covers init, mut, has_one, and constraint in detail.
If you are building a production Solana program and want a review of your account model before deploying to mainnet, the blockchain development service covers smart contract design, security review, and deployment.
Section 07 · FAQ
Common questions about Rust structs in Solana
Direct answers to the questions developers ask most when defining account state for the first time.
What is a Rust struct in Solana?
A Rust struct in Solana defines the shape of data stored inside an account. When you mark a struct with #[account], Anchor treats it as an on chain account type that can be initialised, read, and mutated through instructions. The struct fields describe exactly what state your program tracks, and Anchor uses the Borsh serialisation format to convert those fields to and from raw bytes when reading and writing the account's data array.
What does #[account] do in Anchor?
The #[account] macro in Anchor automatically derives several important traits for an account struct. It implements AnchorSerialize and AnchorDeserialize for Borsh serialisation, AccountSerialize and AccountDeserialize for Anchor's own account wrapper, Discriminator for the 8 byte type identifier, and Owner to associate the account with your program. It also enforces that the 8 byte discriminator is written at the start of the account data on initialisation, which Anchor uses to verify that an account has the correct type before loading it in an instruction.
What is a discriminator in Anchor?
A discriminator is an 8 byte identifier that Anchor automatically writes at the beginning of every account's data when it is initialised. The value is the first 8 bytes of the SHA-256 hash of the string account:StructName, where StructName is the name of your Rust struct. When Anchor loads an account in a subsequent instruction, it checks that the first 8 bytes match the expected discriminator before deserialising the rest of the data. This prevents one account type from being passed where another type is expected, which would otherwise be a critical security vulnerability.
How do I calculate the space for a Solana account struct?
Account space is the number of bytes the account data field needs to hold your serialised struct. The formula is: 8 bytes (the Anchor discriminator) plus the sum of every field's Borsh size. Fixed size types like Pubkey (32 bytes), u64 (8 bytes), and bool (1 byte) have predictable sizes. Variable size types like String and Vec need an upper bound: use #[max_len(N)] with the InitSpace derive, and Anchor computes INIT_SPACE automatically. You pass this value as the space parameter in your init constraint: space = 8 + YourStruct::INIT_SPACE.
What is the difference between Clone and Copy in Rust?
Copy is a marker trait that tells Rust to copy a value automatically on assignment or function call, without consuming the original. It only works for types that fit entirely on the stack and have no heap allocations, like integers, booleans, and Pubkey. Clone is a more general trait that provides an explicit .clone() method for making deep copies, including heap allocated data like String and Vec. In Solana account structs, you derive Clone but typically not Copy, because structs that contain String or Vec cannot implement Copy. Anchor's #[account] macro derives Clone for you automatically.