Borsh Serialization
Borsh (Binary Object Representation Serializer for Hashing) is the canonical encoding for Solana instruction data and most account structs. It is deterministic, little-endian, and length-prefixed for collections.
Recipe
use borsh::{BorshDeserialize, BorshSerialize};
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Deposit {
pub amount: u64,
}
let ix_data = Deposit { amount: 1_000 }.try_to_vec()?;When to reach for this:
- Defining instruction enums and payloads.
- Storing structured state in data accounts.
- Matching Anchor's default account serialization.
Working Example
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program_error::ProgramError,
pubkey::Pubkey,
};
#[derive(BorshSerialize, BorshDeserialize)]
pub enum VaultInstruction {
Deposit { amount: u64 },
Withdraw { amount: u64 },
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct VaultState {
pub total: u64,
}
pub const VAULT_LEN: usize = 8;
pub fn process_deposit(
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let iter = &mut accounts.iter();
let vault = next_account_info(iter)?;
let mut state = VaultState::deserialize(&mut &vault.data.borrow()[..])
.map_err(|_| ProgramError::InvalidAccountData)?;
state.total = state
.total
.checked_add(amount)
.ok_or(ProgramError::InvalidArgument)?;
state.serialize(&mut &mut vault.data.borrow_mut()[..])?;
Ok(())
}What this demonstrates:
- Instruction enum variants encode as a leading
u8discriminant by default. - Account data is read/written through the account byte slice.
- Checked arithmetic guards the state update before serialization.
Deep Dive
How It Works
- Primitives encode in little-endian fixed width.
StringandVec<T>prefix with au32length.- Deserialize consumes bytes sequentially - trailing bytes are ignored unless you validate length.
Size Planning
| Field | Bytes |
|---|---|
u64 | 8 |
Pubkey | 32 |
bool | 1 |
String | 4 + utf8 len |
Rust Notes
// Use try_to_vec for instruction data; check account.data.len() >= size.
const STATE_LEN: usize = core::mem::size_of::<u64>();Gotchas
- Account too small - Serialize panics or truncates if
data.len()is insufficient.. Fix: Allocate exact space at account creation and storeLENconstant. - Schema drift - Adding fields breaks old accounts without migration.. Fix: Version byte or discriminator plus migration instruction.
- Enum reorder - Changing variant order changes discriminants.. Fix: Append new variants; never reorder.
- UTF-8 strings - Invalid UTF-8 fails deserialization.. Fix: Validate off-chain before sending or use fixed byte arrays.
- Borrow conflict - Serializing while another borrow is active panics.. Fix: Drop immutable borrows before
borrow_mut.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| bytemuck / zero-copy | Large fixed structs | Variable strings or vectors |
Anchor Account<T> | Automatic layout checks | Raw control or minimal CU |
| Manual byte packing | Ultra-tight layouts | Maintainability |
FAQs
Is Borsh the same as Anchor uses?
Anchor 0.32.1 uses Borsh for account data by default.
Little or big endian?
Little-endian for primitives.
How are Options encoded?
1-byte tag (0/1) plus inner value when Some.
Can I skip trailing bytes?
Deserializer stops at struct end; validate total length for security.
Max account size?
10 MB cap per account - still pay rent and CUs.
How to compute size?
Serialize default struct on host or use space constant.
Vec in account?
Allowed but grows rent; prefer fixed-capacity arrays for games/order books.
Pubkey in Borsh?
32 raw bytes - same as on wire.
Difference from bincode?
Borsh is canonical for Solana; bincode is not cross-language stable here.
Deserialize untrusted data?
Always check owner, discriminator, and length first.
i128 support?
Supported - 16 bytes little-endian.
Testing roundtrip?
Use try_to_vec then deserialize in unit tests on host.
Related
- Account Discriminators - Type tagging
- Borsh Structs - Layout design
- Zero-Copy with bytemuck - Performance alternative
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.