Create your own
Lesson illustration

Account Data Serialization

Hello! Welcome back to our course on Solana development.

In our last few lessons, we've assembled the core components of a native Solana program. We've learned to:

  • Process incoming instructions in the program's entrypoint.
  • Read and deserialize instruction data using borsh.
  • Access account information via the AccountInfo struct.
  • Perform crucial security validations for account ownership and signer privileges.

So far, our program can read state and make decisions. Now, we need to complete the cycle by learning how to save the results of our logic back to the blockchain. This lesson focuses on our next learning outcome: to serialize and write data to an account's data buffer.

This is the final step in making your program stateful. After performing calculations or processing an instruction, you'll save the new state, ensuring that it persists for future transactions.

The Account Data Buffer

As you'll recall, every Solana account has a data field. From the runtime's perspective, this is just a raw, unstructured array of bytes.

Solana Account Structure with Emphasized Data Field
Each Solana account has its own data buffer. Our program is responsible for defining the structure of these bytes and writing to them.

It's our program's job to impose a structure on this byte array. When we want to save information—like a counter's value or a user's profile—we need to convert our structured Rust data (like a struct) into this byte array format. This process is called serialization.

Serialization with Borsh

We've already used borsh to deserialize instruction data. Now, we'll use it to serialize our account state. The principle is the same: it's a standardized way to convert structured data into a compact binary format and back again.

Given your background in web development, you can think of this as being analogous to JSON.stringify() in JavaScript, which converts a JS object into a string for storage or network transmission. Borsh does the same for Rust structs, but produces a much more compact and efficient byte array.

To make a Rust struct serializable with borsh, we simply use a derive macro.

Rust Program Structure

The official Solana documentation provides a clear example of defining an account's data structure. Let's look at how to define a simple CounterAccount struct.

In the provided resource, find the section titled "Define program state". Observe how the CounterAccount struct is defined and, most importantly, notice the #[derive(BorshSerialize, BorshDeserialize, Debug)] line above it. This single line instructs the Rust compiler to automatically generate the code for serializing and deserializing this struct using borsh.

By adding #[derive(BorshSerialize)], our CounterAccount struct now automatically has a .serialize() method that we can call to convert an instance of the struct into bytes.

Writing Data to an Account

Now let's get to the main event: writing our serialized data into an account's data buffer. The process generally involves these steps:

  1. Get a mutable reference to the account's data buffer.
  2. If updating existing data, first deserialize the current state from the buffer into your struct.
  3. Modify the data in your struct instance.
  4. Call the .serialize() method on your struct to write the new byte representation back into the buffer.

Let's see this in action with a complete example. We'll examine the code for an increment instruction that reads a counter's current value, adds one, and saves it back.

Rust Program Structure

This section of the Solana documentation provides a perfect, self-contained function that demonstrates how to update and re-serialize account data.

Read the code in the section "Implement increment handler". Pay close attention to the following lines in the process_increment_counter function: let mut data = counter_account.data.borrow_mut();: This gets a mutable handle to the account's data buffer. let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;: This deserializes the existing data into our struct. counter_data.count = counter_data.count.checked_add(1)...: This is our program's logic—incrementing the count. counter_data.serialize(&mut &mut data[..])?;: This is the key step where we serialize the modified struct and write it back into the same data buffer.

A Note on the serialize Syntax

The syntax counter_data.serialize(&mut &mut data[..])? might look a bit unusual. Let's break it down:

  • data is a RefMut<[u8]>, a smart pointer that manages the mutable borrow of the account data.
  • data[..] creates a mutable slice (&mut [u8]) that covers the entire data buffer.
  • The serialize method requires a type that implements the std::io::Write trait. A mutable slice does.
  • The double &mut is a Rust pattern to pass the mutable slice correctly into the serialize method.

While the syntax is a bit dense, the concept is straightforward: we're telling borsh to take our counter_data struct and write its byte representation into the memory slice pointed to by data.

Test your understanding!

You're writing a program to store a user's high score. Your account state struct is struct HighScore { score: u32 }. You have an AccountInfo for the user's score account called score_account.

Which of the following code snippets correctly updates the high score to a new value, new_score?

A:

let mut high_score = HighScore { score: new_score };
high_score.serialize(&mut score_account.data.borrow_mut()[..])?;

B:

let mut data = score_account.data.borrow_mut();
let mut high_score = HighScore::try_from_slice(&data)?;
high_score.score = new_score;
high_score.serialize(&mut &mut data[..])?;

C:

let mut high_score = HighScore::try_from_slice(&score_account.data.borrow())?;
high_score.score = new_score;
Show answer

The correct answer is B.

Here's the breakdown:

  • A is incorrect because it tries to serialize a new struct over the old data, but it doesn't first obtain a RefMut (a mutable borrow) from the RefCell. You must call .borrow_mut() first. It also overwrites any other data that might have been in the struct, rather than updating a single field.
  • B is correct. It follows all the steps:
    1. It gets a mutable borrow of the data.
    2. It deserializes the current state from the buffer into a HighScore struct.
    3. It updates the score field.
    4. It serializes the updated struct back into the buffer.
  • C is incorrect because it only reads the data and updates it in memory. It never gets a mutable borrow and never calls .serialize() to write the changes back to the blockchain. The change would be lost when the program finishes.

Initializing an Account vs. Updating It

The process is slightly simpler when you are initializing a brand new account. In that case, there's no existing data to deserialize. You simply create a new instance of your state struct, get a mutable reference to the (empty) data buffer, and serialize your new struct into it.

You can see this pattern in the process_initialize_counter function from the same resource (LINK), in the section titled "Implement initialize handler". There, a new CounterAccount is created and serialized into the newly created account's data buffer without first calling try_from_slice.

Tying it to the Client

So far, we've focused on the on-chain program. But it's helpful to see how the client-side initiates this. A client (e.g., a web app) serializes the instruction data and sends it in a transaction. Our program deserializes this instruction, performs its logic, and then serializes the new account state back to the account.

Let's watch a video that demonstrates this full loop from the client-side perspective. This will help you connect the on-chain logic we're learning to the front-end applications you're experienced with building.

Serializing Instruction Data [Solana Development Course: Module 1, Part 4] - Sept 2nd '22

The video "Serializing Instruction Data" by Solandy provides a great client-side walkthrough. We'll see how a JavaScript client defines a data structure, serializes it using the JavaScript version of borsh, and includes it in a transaction to be sent to a program.

Watch the following three segments: Practical Application: Serializing Movie Review Data (24:48 - 34:46): Observe how a serialize method is created in a JavaScript class to prepare data for the Solana program. This is the client-side equivalent of what our program does, but for instruction data. Sending a Transaction with Serialized Instruction Data (38:27 - 44:58): See how the serialized buffer is added to a TransactionInstruction and sent to the network. Verifying Serialized Data on the Blockchain (48:43 - 50:50): Finally, watch as the transaction is viewed on the Solana Explorer. The instructor decodes the raw bytes in the instruction data, showing exactly how the serialized data ended up on-chain. This provides a great visual confirmation of the whole process.

This client-side view should reinforce what's happening under the hood. The client serializes the command, and the program serializes the result. Both sides must agree on the data structures and serialization format (borsh).

Conclusion

Congratulations! You've now learned the final piece of the puzzle for creating a basic, stateful Solana program. You can now write a program that receives an instruction, reads the current state, validates permissions, executes logic, and saves the new state back to the blockchain.

Here are the key takeaways from this lesson:

  • An account's data field is a raw byte buffer that our program is responsible for managing.
  • Serialization is the process of converting a structured Rust object (like a struct) into a byte array for on-chain storage.
  • The borsh library is the Solana standard for this. We use #[derive(BorshSerialize)] to automatically implement serialization for our structs.
  • To write to an account, you must get a mutable borrow of its data buffer (.data.borrow_mut()), modify your struct in memory, and then call the .serialize() method to write the changes back.

You now have a complete mental model of a single instruction's lifecycle. In our next lesson, we will put all these pieces together as we learn to build and deploy a native Solana program to a local ledger or devnet.

Can't find a good explanation? Sign up and we'll make it for you

Sign up