Skip to main content
A short demo of how it looks:

Key features of auto-serialization

  • Supports all types: unions, tensors, nullables, generics, atomics, …
  • Allows you to specify serialization prefixes (particularly, opcodes)
  • Allows you to manage cell references and when to load them
  • Lets you control error codes and other behavior
  • Unpacks data from a cell or a slice, mutate it or not
  • Packs data to a cell or a builder
  • Warns if data potentially exceeds 1023 bits
  • More efficient than manual serialization

List of supported types and how they are serialized

A small reminder: Tolk has intN types (int8, uint64, etc.). Of course, they can be nested, like nullable int32? or a tensor (uint5, int128). They are just integers at the TVM level, they can hold any value at runtime: overflow only happens at serialization. For example, if you assign 256 to uint8, asm command “8 STU” will fail with code 5 (integer out of range).
  • (1) By analogy with intN, there are bytesN types. It’s just a slice under the hood: the type shows how to serialize this slice. By default, before STSLICE, the compiler inserts runtime checks (get bits/refs count + compare with N + compare with 0). These checks ensure that serialized binary data will be correct, but they cost gas. However, if you guarantee that a slice is valid (for example, it comes from trusted sources), pass an option skipBitsNValidation to disable runtime checks.
  • (2) TL-B Either is expressed with a union T1 | T2. For example, int32 | int64 is packed as (“0” + 32-bit int OR “1” + 64-bit int). However, if T1 and T2 are both structures with manual serialization prefixes, those prefixes are used instead of a 0/1 bit.
  • (3) To pack or unpack a union, say, Msg1 | Msg2 | Msg3, we need serialization prefixes. For structures, you can specify them manually (or the compiler will generate them right here). For primitives, like int32 | int64 | int128 | int256, the compiler generates a prefix tree (00/01/10/11 in this case). Read auto-generating serialization prefixes below.
  • (4) Using raw builder and slice for writing is supported but not recommended, as they do not reveal any information about their internal structure. Auto-generated TypeScript wrappers will be available in the future. However, for a raw slice, a wrapper cannot be generated, as there is no way to predict how to read such a field back. This feature will likely be removed in the future.

Some examples of valid types

Serialization prefixes and opcodes

Declaring a struct, there is a special syntax to provide pack prefixes. Typically, you’ll use 32-bit prefixes for messages opcodes, or arbitrary prefixes is case you’d like to express TL-B multiple constructors.
Prefixes can be of any width, they are not restricted to be 32 bit.
  • 0x000F — 16-bit prefix
  • 0x0F — 8-bit prefix
  • 0b010 — 3-bit prefix
  • 0b00001111 — 8-bit prefix
Declaring messages with 32-bit opcodes does not differ from declaring any other structs. Say, you the following TL-B scheme:
You can express the same with structures and union types:
When deserializing, Asset will follow manually provided prefixes:
When serializing, everything also works as expected:
Note that if a struct has a manual pack prefix, it does not matter whether this struct is inside any union or not.
That’s why, when you declare outgoing messages with 32-bit opcodes and just serialize them, that opcodes are included in binary data.

What can NOT be serialized

  • int can’t be serialized, it does not define binary width; use int32, uint64, etc.
  • slice, for the same reason; use address or bitsN
  • tuples, not implemented
  • A | B (and A|B|C|... in general) if A has manual serialization prefix, B not (because it seems like a bug in your code)
  • int32 | A (and primitives|...|structs in general) if A has manual serialization prefix (because it’s not definite what prefixes to use for primitives)
Example of invalid:

Error messages if serialization unavailable

If you, by mistake, use unsupported types, Tolk compiler will fire a meaningful error. Example:
fires an error:

Controlling cell references. Typed cells

Tolk gives you full control over how your data is placed in cells and how cells reference each other. When you declare fields in a struct, there is no compiler magic of reordering fields, making any implicit references, etc. As follows, whenever you need to place data in a ref, you do it manually. As well as you manually control, when contents of that ref is loaded. There are two types of references: typed and untyped.
When you call NftCollectionStorage.fromSlice (or fromCell), the process is as follows:
  1. read address (slice.loadAddress)
  2. read uint64 (slice.loadUint 64)
  3. read three refs (slice.loadRef); do not unpack them: we just have pointers to cells
Note, that royaltyParams is Cell<T>, not T itself. You can NOT access numerator, etc. To load those fields, you should manually unpack that ref:
And vice versa: when composing such a struct, you should assign a typed cell to a field:
Probably, you’ve guessed that T.toCell() makes Cell<T>, actually. That’s true:
With types cells, you can express snake data:
So, typed cells are a powerful mechanism to express the contents of referenced cells. Note that Cell<address> or even Cell<int32 | int64> is also okay, you are not restricted to structures. When it comes to untyped cells — just cell — they also denote references, but don’t denote their inner contents, don’t have the .load() method. It’s just some cell, like code/data of a contract or an untyped nft content.

Remaining data after reading

Suppose you have struct Point (x int8, y int8), and read from a slice with contents 0102FF. Byte 01 for x, byte 02 for y, and the remaining FF — is it correct? By default, this is incorrect. By default, functions fromCell and fromSlice ensure the slice end after reading. In this case, exception 9 (“cell underflow”) is thrown. But you can override this behavior with an option:

Custom serializers for custom types

Imagine that you have your own “variable-length string”: you encode its len, and then data, like
To express this, you can create your own type and define a custom serializer:
And just use VariadicString as a regular type — everywhere:
The compiler inlines your functions every time packing/unpacking takes place. Method names packToBuilder and unpackFromSlice are reserved for this purpose, their prototype must be exactly as showed.

UnpackOptions and PackOptions

They allow you to control behavior of fromCell, toCell, and similar functions:
Serialization functions have the second optional parameter, actually:
When you don’t pass it, default options are used. But you can overload some of the options. For deserialization (fromCell and similar), there are now two available options: For serialization (toCell and similar), there is now one option:

Full list of serialization functions

Each of them can be controlled by PackOptions described above.
  1. T.toCell() — convert anything to a cell. Example:
Internally, a builder is created, all fields are serialized one by one, and a builder is flushed (beginCell() + serialize fields + endCell()).
  1. builder.storeAny<T>(v) — similar to builder.storeUint() and others, but allows storing structures. Example:

Full list of deserialization functions

Each of them can be controlled by UnpackOptions described above.
  1. T.fromCell(c) — parse anything from a cell. Example:
Internally, a cell is unpacked to a slice, and that slice is parsed (c.beginParse() + read from slice).
  1. T.fromSlice(s) — parse anything from a slice. Example:
All fields are read from a slice immediately. If a slice is corrupted, an exception is thrown (most likely, excode 9 “cell underflow”). Note, that a passed slice is NOT mutated; its internal pointer is NOT shifted. If you need to mutate it, like cs.loadInt(), consider calling cs.loadAny<Increment>().
  1. slice.loadAny<T> — parse anything from a slice, shifting its internal pointer. Similar to slice.loadUint() and others, but allows loading structures. Example:
Similar to MyStorage.fromSlice(cs), but called as a method and mutates the slice. Note: options.assertEndAfterReading is ignored by this function, because it’s actually intended to read data from the middle.
  1. slice.skipAny<T> — skip anything in a slice, shifting its internal pointer. Similar to slice.skipBits() and others, but allows skipping structures. Example:

Special type RemainingBitsAndRefs

It’s a built-in type to get “all the rest” slice tail on reading. Example:
When you deserialize JettonMessage, forwardPayload contains everything left after reading fields above. Essentially, it’s an alias to a slice which is handled specially while unpacking:

Auto-generating prefixes for unions

We’ve mentioned multiple times, that T1 | T2 is encoded as TL-B Either: bit ‘0’ + T1 OR bit ‘1’ + T2. But what about wider unions? Say,
In this case, the compiler auto-generates a prefix tree. This field will be packed as: ‘00’ + int8 OR ‘01’ + int16 OR ‘10’ + int32. On deserialization, the same format is expected (prefix ‘11’ will throw an exception). The rules:
  • if null exists, it’s 0, all others are 1+tree: A|B|C|D|null => 0 | 100+A | 101+B | 110+C | 111+D
  • if no null, just distributed sequentially: A|B|C => 00+A | 01+B | 10+C
Same for structs without a manually specified prefix:
When declaring a struct, you can manually specify serialization prefix (32-bit prefixes for messages are called opcodes, but prefixes in general can be of any length):
If you specify prefixes manually, they will be used in (de)serialization. Moreover: since a prefix exists for a struct, when deserializing a struct itself (not inside a union), a prefix is expected to be contained in binary data:
So, the rules are quite simple:
  • if you specify prefixes manually, they will be used (no matter within a union or not)
  • if you don’t specify any prefixes, the compiler auto-generates a prefix tree
  • if you specify prefix for A, but forgot prefix for B, A | B can’t be serialized
  • either bit (0/1) is just a prefix tree for two cases
How can I specify a serialization prefix for non-struct? Currently, there is no way to write something like Prefixed<int32, 0b0011>. But you can just create a struct with a single field:
It will have no overhead against just int32, the same slot on a stack, just adding a prefix for (de)serialization.

What if data exceeds 1023 bits

Struct fields are serialized one-by-one. So, if you have a large structure, its content may not fit into a cell. Tolk compiler calculates the maximum size of every serialized struct, and if it potentially exceeds 1023 bits, fires an error. Your choice is
  1. either to suppress the error by placing an annotation above a struct; it means “okay, I understand”
  2. or repack your data by splitting into multiple cells
Why do we say “potentially”? Because for many types, their size can vary:
  • int8? is either one or nine bits
  • coins is variadic: from 4 bits (small values) up to 124 bits
  • any_address is internal (267 bits), or external (up to 521 bits), or none (2 bits)
So, suppose you have:
When you try to (de)serialize it, the compiler calculates its size, and prints an error:
Actually, you have two choices:
  1. you definitely know, that coins fields will be relatively small, and this struct will 100% fit in reality; then, suppress the error using an annotation:
  1. or you really expect billions of billions in coins, so data really can exceed; in this case, you should extract some fields into a separate cell; for example, store 800 bits as a ref; or extract other 2 fields and ref them:
Generally, you leave more frequently used fields directly and place less-frequent fields to another ref. All in all, the compiler indicates on potential cell overflow, and it’s your choice how to overcome this. Probably, in the future, there will be more policies (besides “suppress”) — for example, to auto-repack fields. For now, it’s absolutely straightforward.

Integration with message sending

Auto-serialization is natively integrated with sending messages to other contracts. You just “send some object,” and the compiler automatically serializes it, detects whether it fits into a message cell, etc.
Continue reading here: Universal createMessage.

Not “fromCell” but “lazy fromCell”

Tolk has a special keyword lazy that is combined with auto-deserialization. The compiler will load not a whole struct, but only fields requested.
Continue reading here: lazy loading, partial loading.