Sui.

Post

Share your knowledge.

article banner.
0xduckmove.
May 30, 2025
Article

BCS Encoding in Sui What It Is and Why It Matters

If you’re building on Sui or tinkering with Move, you’ve probably heard the term BCS floating around. That’s short for Binary Canonical Serialization formatting machine originally crafted for the Diem blockchain, and now a cornerstone of Move-based ecosystems like Sui, Aptos, Starcoin, and 0L.

So yeah, you better get cozy with it if you’re serious about building in this space.

What Is BCS?

Binary Canonical Serialization (BCS) is a format used to serialize (encode) and deserialize (decode) structured data into bytes.

You’ll see it used when:

  • Encoding transactions before signing.
  • Emitting or parsing events from the blockchain.
  • Interacting with Move smart contracts off-chain via JavaScript.

But BCS doesn’t include type information in the bytes. This means you must know the structure ahead of time when decoding unlike formats like JSON or Protocol Buffers, which are more self-describing.

Key Features of BCS

No Type Metadata

The serialized output contains no hints about what types the fields are. You must know what you’re dealing with when you decode.

Order-Dependent Serialization

Structs are encoded in the exact order of their fields. Change the order, and your deserialization breaks. This is why peel_* functions in Move must match the struct’s layout 1:1.

Generic Type

In a struct like:

struct BCSObject<T> has drop, copy {
    id: ID,
    owner: address,
    meta: Metadata,
    generic: T
}

You can only deserialize reliably up to the meta field. Generic types mess with BCS parsing, so always put them last if you want your data to be safely decoded.

Using BCS in JavaScript

Thanks to the @mysten/bcs library, you can work with BCS in JS like a pro.

npm i @mysten/bcs

and basic example:

import { BCS, getSuiMoveConfig } from "@mysten/bcs";
const bcs = new BCS(getSuiMoveConfig());

const ser = bcs.ser(BCS.U16, 10);
console.log(ser.toBytes()); // [0x0a, 0x00]

const des = bcs.de(BCS.U16, ser.toBytes());
console.log(des); // 10

You can also serialize vectors and strings:

bcs.ser("vector<u8>", [1, 2, 3, 4]); // 04 01 02 03 04
bcs.ser(BCS.STRING, "test string"); // 0b7465737420737472696e67

Registering Custom Types

Let’s say you have the following Move structs:

struct Metadata has drop, copy {
    name: std::ascii::String
}

struct BCSObject has drop, copy {
    id: ID,
    owner: address,
    meta: Metadata
}


You can register them like this in JS:

bcs.registerStructType("Metadata", {
  name: BCS.STRING,
});

bcs.registerStructType("BCSObject", {
  id: BCS.ADDRESS,
  owner: BCS.ADDRESS,
  meta: "Metadata",
});

Serialization & Deserialization Example

JavaScript Serialization:

const bytes = bcs
  .ser("BCSObject", {
    id: "0x0000000000000000000000000000000000000005",
    owner: "0x000000000000000000000000000000000000000a",
    meta: { name: "aaa" }
  })
  .toString("hex");

console.log("Hex:", bytes);

The output maybe:

0x0000000000000000000000000000000000000005000000000000000000000000000000000000000a03616161

This can now be passed into a Move contract or even manually tested in Sui CLI.

BCS may look low-level and byte-heavy, but once you understand how it encodes data, you’ll unlock a deeper understanding of how Move smart contracts really tick — and how to bridge on-chain ↔ off-chain systems safely.

And if you’re debugging BCS bytes on Sui Explorer (like the one below):

BCS Encoding Binary Canonical Serialization, or BCS, is a serialization format developed in the context of the Diem blockchain, and is now extensively used in most of the blockchains based on Move (Sui, Starcoin, Aptos, 0L). BCS is not only used in the Move VM, but also used in transaction and event coding, such as serializing transactions before signing, or parsing event data.

Knowing how BCS works is crucial if you want to understand how Move works at a deeper level and become a Move expert. Let's dive in.

BCS Specification and Properties There are some high-level properties of BCS encoding that are good to keep in mind as we go through the rest of the lesson:

BCS is a data-serialization format where the resulting output bytes do not contain any type information; because of this, the side receiving the encoded bytes will need to know how to deserialize the data

There are no structs in BCS (since there are no types); the struct simply defines the order in which fields are serialized

Wrapper types are ignored, so OuterType and UnnestedType will have the same BCS representation:

struct OuterType { owner: InnerType } struct InnerType { address: address } struct UnnestedType { address: address } Types containing the generic type fields can be parsed up to the first generic type field. So it's a good practice to put the generic type field(s) last if it's a custom type that will be ser/de'd.

struct BCSObject has drop, copy { id: ID, owner: address, meta: Metadata, generic: T } In this example, we can deserialize everything up to the meta field.

Primitive types like unsigned ints are encoded in Little Endian format

Vector is serialized as a ULEB128 length (with max length up to u32) followed by the content of the vector.

The full BCS specification can be found in the BCS repository.

Using the @mysten/bcs JavaScript Library Installation The library you will need to install for this part is the @mysten/bcs library. You can install it by typing in the root directory of a node project:

npm i @mysten/bcs Basic Example Let's use the JavaScript library to serialize and de-serialize some simple data types first:

import { BCS, getSuiMoveConfig } from "@mysten/bcs";

// initialize the serializer with default Sui Move configurations const bcs = new BCS(getSuiMoveConfig());

// Define some test data types const integer = 10; const array = [1, 2, 3, 4]; const string = "test string"

// use bcs.ser() to serialize data const ser_integer = bcs.ser(BCS.U16, integer); const ser_array = bcs.ser("vector", array); const ser_string = bcs.ser(BCS.STRING, string);

// use bcs.de() to deserialize data const de_integer = bcs.de(BCS.U16, ser_integer.toBytes()); const de_array = bcs.de("vector", ser_array.toBytes()); const de_string = bcs.de(BCS.STRING, ser_string.toBytes()); We can initialize the serializer instance with the built-in default setting for Sui Move using the above syntax, new BCS(getSuiMoveConfig()).

There are built-in ENUMs that can be used for Sui Move types like BCS.U16, BCS.STRING, etc. For generic types, it can be defined using the same syntax as in Sui Move, like vector in the above example.

Let's take a close look at the serialized and deserialized fields:

ints are little-endian hexadecimals

0a00 10

the first element of a vector indicates the total length,

then it's just whatever elements are in the vector

0401020304 1,2,3,4

strings are just vectors of u8's, with the first element equal to the length of the string

0b7465737420737472696e67 test string Type Registration We can register the custom types we will be working with using the following syntax:

import { BCS, getSuiMoveConfig } from "@mysten/bcs"; const bcs = new BCS(getSuiMoveConfig());

// Register the Metadata Type bcs.registerStructType("Metadata", { name: BCS.STRING, });

// Same for the main object that we intend to read bcs.registerStructType("BCSObject", { // BCS.ADDRESS is used for ID types as well as address types id: BCS.ADDRESS, owner: BCS.ADDRESS, meta: "Metadata", }); Using bcs in Sui Smart Contracts Let's continue our example from above with the structs.

Struct Definition We start with the corresponding struct definitions in the Sui Move contract.

{ //.. struct Metadata has drop, copy { name: std::ascii::String }

struct BCSObject has drop, copy {
    id: ID,
    owner: address,
    meta: Metadata
}
//..

} Deserialization Now, let's write the function to deserialize an object in a Sui contract.

public fun object_from_bytes(bcs_bytes: vector<u8>): BCSObject {

    // Initializes the bcs bytes instance
    let bcs = bcs::new(bcs_bytes);

    // Use `peel_*` functions to peel values from the serialized bytes. 
    // Order has to be the same as we used in serialization!
    let (id, owner, meta) = (
    bcs::peel_address(&mut bcs), bcs::peel_address(&mut bcs), bcs::peel_vec_u8(&mut bcs)
    );
    // Pack a BCSObject struct with the results of serialization
    BCSObject { id: object::id_from_address(id), owner, meta: Metadata {name: std::ascii::string(meta)}  } }

The varies peel_* methods in Sui Frame bcs module are used to "peel" each individual field from the BCS serialized bytes. Note that the order we peel the fields must be exactly the same as the order of the fields in the struct definition.

Quiz: Why are the results not the same from the first two peel_address calls on the same bcs object?

Also note how we convert the types from address to id, and from vector<8> to std::ascii::string with helper functions.

Quiz: What would happen if BSCObject had a UID type instead of an ID type?

Complete Ser/De Example Find the full JavaScript and Sui Move sample codes in the example_projects folder.

First, we serialize a test object using the JavaScript program:

// We construct a test object to serialize, note that we can specify the format of the output to hex let _bytes = bcs .ser("BCSObject", { id: "0x0000000000000000000000000000000000000005", owner: "0x000000000000000000000000000000000000000a", meta: {name: "aaa"} }) .toString("hex"); We want the BCS writer's output to be in hexadecimal format this time, which can be specified like above.

Affix the serialization result hexstring with 0x prefix and export to an environmental variable:

export OBJECT_HEXSTRING=0x0000000000000000000000000000000000000005000000000000000000000000000000000000000a03616161 Now we can either run the associated Move unit tests to check for correctness:

sui move test You should see this in the console:

BUILDING bcs_move Running Move unit tests [ PASS ] 0x0::bcs_object::test_deserialization Test result: OK. Total tests: 1; passed: 1; failed: 0 Or we can publish the module (and export the PACKAGE_ID) and call the emit_object method using the above BCS serialized hexstring:

sui client call --function emit_object --module bcs_object --package $PACKAGE_ID --args $OBJECT_HEXSTRING We can then check the Events tab of the transaction on the Sui Explorer to see that we emitted the correctly deserialized BCSObject:

  • Sui
  • SDKs and Developer Tools
1
Share
Comments
.
harry phan.
May 30 2025, 17:12

What is little-endian and big-endian?

Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.

306Posts450Answers
Sui.X.Peera.

Earn Your Share of 1000 Sui

Gain Reputation Points & Get Rewards for Helping the Sui Community Grow.

Reward CampaignJune
We use cookies to ensure you get the best experience on our website.
More info