How to Use Custom Dataclasses in GenLayer Contract Storage: A Complete Guide

Use the @allow_storage decorator before @dataclass to mark custom Python classes as safe for GenLayer contract storage, then reference them in TreeMap, DynArray, or other storage containers with type-annotated fields.

GenLayer contracts rely on deterministic, on‑chain data structures for persistent state. While the platform provides built‑in storage types like TreeMap, DynArray, Array, u256, and i256, developers often need richer data models. The @allow_storage decorator enables custom Python dataclasses to be embedded directly in contract storage, with the GenVM handling automatic serialization and deserialization.

Understanding the @allow_storage Decorator

The @allow_storage decorator signals to the GenVM linter that a class is safe for serialization and can persist in contract state. This marker is required before the class can be used as a field type or container value in any storage declaration.

Critical requirements for decorated classes:

  • Must appear before @dataclass in the decorator stack
  • All fields require storage‑compatible type annotations
  • Must use the generated __init__ from @dataclass — no custom __init__ logic allowed
  • Nested dataclasses must also be marked with @allow_storage

Defining a Storage-Compatible Dataclass

Follow this pattern to create valid storage classes in your GenLayer contracts, as demonstrated in [contracts/football_bets.py](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py#L8-L24):

from dataclasses import dataclass
from genlayer import *

@allow_storage                 # Must precede @dataclass

@dataclass
class Bet:
    id: str
    has_resolved: bool
    game_date: str
    resolution_url: str
    team1: str
    team2: str
    predicted_winner: str
    real_winner: str
    real_score: str

Each field uses primitive storage types (str, bool). For nested structures, recursively apply @allow_storage to all dataclass components.

Using Dataclasses in Contract Storage Declarations

Once marked, reference your dataclass in storage type annotations. The FootballBets contract in the boilerplate repository shows nested storage with custom dataclasses:

class FootballBets(gl.Contract):
    bets: TreeMap[Address, TreeMap[str, Bet]]   # Bet used as nested value type

    points: TreeMap[Address, u256]

The TreeMap requires storage‑compatible key and value types. Bet satisfies this constraint because it is an allowed storage class.

Creating and Persisting Dataclass Instances

Inside contract methods, instantiate and assign dataclasses directly. The GenVM automatically serializes on assignment:

bet = Bet(
    id=bet_id,
    has_resolved=False,
    game_date=game_date,
    resolution_url=match_resolution_url,
    team1=team1,
    team2=team2,
    predicted_winner=predicted_winner,
    real_winner="",
    real_score="",
)
self.bets.get_or_insert_default(sender_address)[bet_id] = bet

The get_or_insert_default method returns a mutable TreeMap for the address, allowing direct indexed assignment of the Bet instance.

Reading Stored Dataclass Values

Access stored dataclasses through normal indexing. Fields are available as attributes:

stored_bet = self.bets[gl.message.sender_address][bet_id]
print(stored_bet.real_winner)   # Direct field access

The GenVM deserializes the stored representation back to a Python dataclass instance automatically.

Complete Working Example

This standalone contract demonstrates the full pattern for using custom dataclasses in GenLayer storage:


# contracts/example_contract.py

from dataclasses import dataclass
from genlayer import *

@allow_storage
@dataclass
class Profile:
    username: str
    score: u256
    active: bool

class GameStats(gl.Contract):
    profiles: TreeMap[Address, Profile]

    @gl.public.write
    def set_profile(self, username: str, score: int, active: bool) -> None:
        prof = Profile(username=username, score=score, active=active)
        self.profiles[gl.message.sender_address] = prof

    @gl.public.view
    def get_profile(self, addr: str) -> dict:
        prof = self.profiles[Address(addr)]
        return {"username": prof.username, "score": int(prof.score), "active": prof.active}

The Profile dataclass stores a username, score, and active status per address. The set_profile method creates and persists instances; get_profile demonstrates field access in view functions.

Storage Type Compatibility and Restrictions

Aspect Requirement
Decorator order @allow_storage must precede @dataclass
Field types Only primitives (str, bool, int), u256, i256, other @allow_storage dataclasses, or allowed containers (TreeMap, DynArray, Array)
Mutability Python list and dict are prohibited unless wrapped in DynArray or TreeMap
Defaults Avoid mutable default values; use dataclass generated __init__ or immutable defaults
Linter enforcement genvm-lint raises errors for unmarked classes used in storage

Key Source Files in the Boilerplate Repository

File Purpose
[contracts/football_bets.py](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py) Real‑world Bet dataclass with nested TreeMap storage
[CLAUDE.md](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/CLAUDE.md) Official documentation of supported storage types and @allow_storage
[README.md](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/README.md) Contract development workflow overview
[frontend/lib/contracts/FootballBets.ts](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/frontend/lib/contracts/FootballBets.ts) TypeScript client accessing stored dataclass data

Summary

  • Apply @allow_storage before @dataclass to enable storage compatibility
  • Type-annotate all fields with storage‑compatible types only
  • Use @allow_storage recursively for any nested dataclasses
  • Assign instances directly to TreeMap, DynArray, or Array containers — GenVM handles serialization
  • Access fields directly on retrieved instances; no manual deserialization required
  • Run genvm-lint to catch storage violations before deployment

Frequently Asked Questions

What happens if I forget the @allow_storage decorator?

The genvm-lint tool will raise a compilation error when your dataclass is used in a storage type annotation. The contract will fail to build until the decorator is added in the correct position before @dataclass.

Can I use regular Python list or dict inside a dataclass for GenLayer storage?

No. Standard Python containers are not deterministic and cannot be stored directly. Use DynArray for variable-length sequences or TreeMap for key-value mappings. These GenLayer types provide the required serialization guarantees for on-chain state.

Does @allow_storage support inheritance between dataclasses?

As implemented in genlayerlabs/genlayer-project-boilerplate, each dataclass in the hierarchy must independently declare @allow_storage. The linter validates storage compatibility field-by-field, so parent classes without the decorator will cause compilation failures even if children are properly marked.

Can I add methods to my @allow_storage dataclass?

Yes, methods are permitted. However, you cannot override __init__ with custom logic — the generated __init__ from @dataclass must remain intact for the GenVM to correctly instantiate stored objects during deserialization.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →