Add how-to guide for storage data migration (#2228) by carstenjacobsen · Pull Request #2299 · stellar/stellar-docs

## Why intuitive approaches fail
Suppose a contract stores `DataV1` entries and is upgraded to use `DataV2`, which adds an optional field `c`:
```rust #[contracttype] pub struct Data { a: i64, b: i64 }
#[contracttype] pub struct DataV2 { a: i64, b: i64, c: Option<i64> } ```
### Approach 1: Read old entries directly with the new type
The most natural approach is to read the stored bytes directly as `DataV2` and expect `c` to default to `None`:
```rust // Reading a DataV1 entry with the DataV2 type. // A developer might expect c = None for old entries — but this traps. let data: DataV2 = env.storage().persistent().get(&key).unwrap(); // Error(Object, UnexpectedSize) ```
This traps with `Error(Object, UnexpectedSize)`. The Soroban host validates the field count of the XDR-encoded value against the type definition before returning anything to the contract. Because `DataV1` has two fields and `DataV2` has three, the host rejects the entry before the SDK can handle it.
### Approach 2: Use `try_from_val` as a fallback
Another approach is to use `try_from_val` expecting to catch a deserialization error and recover:
```rust let raw: Val = env.storage().persistent().get(&key).unwrap(); if let Ok(v2) = DataV2::try_from_val(&env, &raw) { v2 } else { // This branch is never reached — the host traps before returning Err. let v1 = DataV1::try_from_val(&env, &raw).unwrap(); DataV2 { a: v1.a, b: v1.b, c: None } } ```
This also traps at the host level. The field count validation happens in the host environment during deserialization — it does not produce a Rust `Err` that the SDK can intercept. There is no way to catch or recover from the mismatch at the contract level.
The root issue is that a contract cannot determine which type an existing storage entry was written as just by reading it. That information must be stored explicitly.