Learn how to create your own data types and turn them into real objects on the Sui blockchain. We'll break down structs, abilities, and the object model step by step — no prior experience needed.
A struct is like a blueprint or a form — it defines what information goes together. Think of a driver's license: it has your name, photo, and birthdate all on one card. That's exactly what a struct does in code — it bundles related pieces of data into one neat package. In Sui Move, a struct is the only way to create your own custom type. You define one inside a module using the `struct` keyword, give it a name (always starting with an uppercase letter, like `Profile` or `Sword`), and list the fields it contains. Each field has a name and a type, separated by a colon. You access fields using dot notation (like `profile.name`). Let's see what this looks like:
Here's the thing — in Move, you can't just do whatever you want with data. Every struct has a set of "abilities" that act like permissions. Think of abilities as rules about what you're allowed to do with a piece of data. Can it be copied? Can it be thrown away? Can it live on the blockchain as its own thing? There are exactly four abilities: - **copy**: The value can be duplicated — like photocopying a document. Numbers have this by default. - **drop**: The value can be silently discarded when you're done with it. Without drop, you MUST explicitly do something with the value — you can't just ignore it. - **store**: The value can be stored inside another struct or placed into global storage. Think of it as "this can go in a filing cabinet." - **key**: This is the big one. It turns your struct into a real Sui object that lives on the blockchain. A struct with `key` MUST have `id: UID` as its first field. You declare abilities with the `has` keyword after the struct name. Let's match them up:
You can mix and match abilities depending on what behavior you want. This is actually one of the most important design decisions you'll make in Sui Move — it determines how your data can be used. Let's look at the common recipes: - `has key, store` -- The most common pattern for objects you want users to trade or transfer freely. The `key` makes it a Sui object, and `store` lets anyone transfer it. Most NFTs and tokens use this. - `has store` -- A value that can only live nested inside another object — like a component inside a machine. It can't exist on its own on the blockchain. - `has copy, drop, store` -- A lightweight data type that can be freely copied, thrown away, and stored. Great for configs, records, or events. - `has key` (without store) -- A "soul-bound" object. Only the module that created it can transfer it. Perfect for identity tokens or badges that shouldn't be tradeable. Click each combination below to learn more:
Let's see what turns a regular struct into a real Sui object that lives on the blockchain. The recipe is simple — two ingredients: 1. Give the struct the `key` ability 2. Make its first field `id: UID` Every Sui object needs a unique ID — like how every house has a unique address. Without it, the blockchain wouldn't know which object is which. You create a UID by calling `object::new(ctx)`, where `ctx` is the transaction context that Sui automatically provides. Here's an important rule that catches a lot of beginners: once you create a Sui object, you MUST explicitly do something with it — transfer it to someone, share it, or freeze it. You can't just create it and walk away. Why? Because the UID inside doesn't have the `drop` ability, so the compiler forces you to handle it. This is actually a great safety feature — it prevents objects from getting lost!
Just like in real life, things on Sui can be owned differently. Your phone is personally yours — only you can use it. A park bench is shared — anyone can sit on it. A bronze statue in a museum is permanent and read-only — everyone can look, nobody can change it. Sui has four ownership models that work the same way: 1. **Address-Owned**: The most common. An object belongs to a specific person, and only they can use it in transactions. Like your phone in your pocket. 2. **Shared**: Any address can read and mutate the object. Created via `transfer::share_object`. Like a public whiteboard anyone can write on. (Heads up: shared objects are a bit slower because they require network consensus.) 3. **Immutable (Frozen)**: The object can never be changed again. Anyone can read it. Created via `transfer::freeze_object`. Like publishing a book — once it's printed, the words don't change. 4. **Wrapped (Child)**: The object is stored as a field inside another object. It doesn't have its own independent identity until it's unwrapped. Like a battery inside a remote control. Let's match the transfer functions to their ownership models:
Now let's see the full lifecycle of a Sui object — from birth to ownership. It follows a clear pattern: 1. **Pack it**: Create a new value by filling in all the fields (including a fresh UID) 2. **Use it**: Read or modify fields with dot notation 3. **Place it**: Transfer, share, or freeze it When you create a struct value, you use this syntax: `TypeName { field1: value1, field2: value2 }`. There's a nice shortcut too: if your variable name matches the field name, you can just write `TypeName { field1, field2 }` instead of repeating yourself. To destroy an object (take it apart), you "unpack" or destructure it: `let TypeName { id, damage, durability } = sword;`. This gives you each field as a separate variable. Fun fact: unpacking is the ONLY way to get at the `id` field so you can delete it with `object::delete(id)`. This is how you permanently destroy a Sui object.
The `init` function is like a grand opening ceremony — it runs exactly once when you first publish your module to the blockchain, and it's your chance to set things up. Need to create an admin badge? Do it in init. Need to set up a global config? Do it in init. After that first run, init can never be called again. There's also a cool concept called a One-Time Witness (OTW). It's an auto-generated struct whose name matches your module name in ALL_CAPS. For example, if your module is called `game`, the OTW is `GAME`. It has only the `drop` ability and is created exactly once by the Sui runtime. Why does this matter? It gives you a cryptographic guarantee that certain setup code can only run once — ever. This is how Sui prevents someone from re-running your initialization logic. The pattern looks like: `fun init(witness: MODULE_NAME, ctx: &mut TxContext) { ... }`
Here's a really important pattern you'll use all the time: the capability pattern. Instead of checking "are you the admin?" by looking at someone's address, you check "do you have the admin badge?" It's like how a security badge gets you into a building — whoever holds it has access. You don't check people's names at the door; you check if they have the right badge. Here's how it works: you define a capability struct (like `AdminCap`), create it in the `init` function, and give it to the deployer. Then in any privileged function, you require the caller to pass in their `AdminCap` as a parameter. If they don't have one, they can't call the function. Simple as that. Why is this better than checking addresses? Because capabilities can be transferred to a new admin, shared for governance, or destroyed to revoke access. If you hard-coded an address, you'd have to redeploy the whole contract to change admins.
Let's review the complete recipe for building a Sui module with objects. Think of this as your checklist: 1. **Define your struct** with the right abilities. Use `key, store` for objects users can trade, `key` alone for soul-bound objects. 2. **Always include `id: UID`** as the first field of any struct with `key`. The compiler will reject your code if you forget this. 3. **Create objects** by packing the struct and using `object::new(ctx)` for the UID. 4. **Transfer ownership** using the correct function: `public_transfer` for `key + store` objects, `transfer` for `key`-only objects. Using the wrong one is a common mistake! 5. **Use the capability pattern** for access control — give out badges, don't check IDs. 6. **Use `init`** to set up singleton objects and hand out initial capabilities. Remember the golden rule: every Sui object must be explicitly placed — transferred, shared, or frozen. You cannot let objects vanish into thin air. Click below for the most common mistakes beginners make: