Migrating from Manual Borsh to LUMOS: A Step-by-Step Guide

Published: (December 17, 2025 at 06:15 PM EST)
2 min read
Source: Dev.to

Source: Dev.to

Cover image for Migrating from Manual Borsh to LUMOS: A Step-by-Step Guide

Why Migrate?

  • Zero runtime overhead – generated code is identical to manual Borsh
  • Type synchronization – TypeScript and Rust always match
  • Reduced maintenance – single source of truth
  • Fewer bugs – eliminates serialization mismatches

Migration time: 5‑30 minutes per struct/enum

Step 1: Audit Your Existing Code

Find All Borsh Types

Rust

# Find structs with Borsh derives
rg '#\[derive.*Borsh' --type rust

# Find Account macros
rg '#\[account\]' --type rust

TypeScript

# Find Borsh schemas
rg 'borsh\.(struct|rustEnum)' --type ts

Step 2: Before & After Example

Before (Manual Borsh)

Rust (state.rs)

use anchor_lang::prelude::*;

#[account]
pub struct PlayerAccount {
    pub wallet: Pubkey,
    pub level: u16,
    pub experience: u64,
    pub items: Vec,
}

TypeScript (types.ts)

import { PublicKey } from '@solana/web3.js';
import * as borsh from '@coral-xyz/borsh';

export interface PlayerAccount {
  wallet: PublicKey;
  level: number;
  experience: number;
  items: PublicKey[];
}

export const PlayerAccountSchema = borsh.struct([
  borsh.publicKey('wallet'),
  borsh.u16('level'),
  borsh.u64('experience'),
  borsh.vec(borsh.publicKey(), 'items'),
]);

After (LUMOS Schema)

Create schema/player.lumos:

#[solana]
#[account]
struct PlayerAccount {
    wallet: PublicKey,
    level: u16,
    experience: u64,
    items: [PublicKey],
}

Generate code:

lumos generate schema/player.lumos

Both Rust and TypeScript are generated automatically.

Step 3: Update Imports

Rust

// OLD:
use crate::state::PlayerAccount;

// NEW:
mod generated;
use generated::PlayerAccount;

TypeScript

// OLD:
import { PlayerAccount, PlayerAccountSchema } from './types';

// NEW:
import { PlayerAccount, PlayerAccountBorshSchema } from './generated';

Step 4: Verify Binary Compatibility

Critical: Ensure LUMOS‑generated code produces identical bytes.

#[test]
fn test_borsh_compatibility() {
    let player = PlayerAccount {
        wallet: Pubkey::new_unique(),
        level: 10,
        experience: 5000,
        items: vec![Pubkey::new_unique()],
    };

    // Serialize with manual implementation
    let manual_bytes = player.try_to_vec().unwrap();

    // Serialize with LUMOS‑generated implementation
    let generated_bytes = player.try_to_vec().unwrap();

    assert_eq!(manual_bytes, generated_bytes, "Binary output must match!");
}

Type Mapping Reference

Manual RustManual TypeScriptLUMOS
PubkeyPublicKeyPublicKey
u16numberu16
u64numberu64
VecT[][T]
OptionT | undefinedOption
StringstringString
boolbooleanbool

Migration Checklist

  • Install LUMOS CLI: cargo install lumos-cli
  • Create .lumos schema files
  • Generate Rust and TypeScript code
  • Update imports in existing code
  • Run binary compatibility tests
  • Remove old manual type definitions
  • Update CI/CD to regenerate on schema changes

Gradual Migration Strategy

You don’t have to migrate everything at once:

  • Phase 1: New types → use LUMOS from the start
  • Phase 2: High‑churn types → migrate types you change often
  • Phase 3: Stable types → migrate remaining types

CI/CD Integration

# .github/workflows/codegen.yml
name: Generate LUMOS Code

on: [push, pull_request]

jobs:
  generate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: cargo install lumos-cli
      - run: lumos generate schema/*.lumos
      - run: git diff --exit-code || (echo "Generated files outdated!" && exit 1)

Get Started

cargo install lumos-cli
lumos generate your-schema.lumos

Documentation:
GitHub:

Back to Blog

Related posts

Read more »