Quick answer: Standardize on a portable serialization format (JSON, MessagePack, or FlatBuffers) with a version header in every save file. Write explicit migration functions for each schema version. Build a transfer test matrix that covers every platform pair — create a save on platform A, transfer to platform B, verify it loads with correct game state. Automate the verification step with checksum comparison so you catch regressions in CI.
Cross-save sounds simple: play on your PC, continue on your phone. In practice, it is a minefield of serialization edge cases. The save file that works perfectly on Windows produces garbage on Switch. The save from version 1.2 crashes version 1.3 on iOS but not Android. The player’s 40-hour RPG save corrupts during transfer and there is no backup. Here’s how to test cross-save thoroughly enough to sleep at night.
Schema Versioning
Every save file must contain a version number. This is non-negotiable. Without it, you have no way to know what format a save file uses, and no way to migrate it when the format changes. Embed the version in the first bytes of the file so you can read it before attempting deserialization.
// SaveHeader.cs — portable save header
public struct SaveHeader
{
public const uint MAGIC = 0x42554731; // "BUG1"
public const int CURRENT_VERSION = 7;
public uint Magic;
public int Version;
public long TimestampUtc;
public int PayloadLength;
public uint PayloadChecksum;
public static SaveHeader Read(BinaryReader reader)
{
var h = new SaveHeader();
h.Magic = reader.ReadUInt32();
if (h.Magic != MAGIC)
throw new InvalidDataException("Not a valid save file");
h.Version = reader.ReadInt32();
h.TimestampUtc = reader.ReadInt64();
h.PayloadLength = reader.ReadInt32();
h.PayloadChecksum = reader.ReadUInt32();
return h;
}
}
When you change the save format — adding a new field, renaming a field, changing the structure of an array — increment the version number and write a migration function that converts version N to version N+1. Chain the migrations so a version 3 save can be upgraded to version 7 by running migrations 3→4, 4→5, 5→6, and 6→7 in sequence. Never skip versions.
Keep a library of test saves at every historical version. When you add migration code, run every old save through the full migration chain and verify the result. This is your regression safety net.
Endianness and Binary Portability
If you use a binary save format, you must handle byte order explicitly. Most modern platforms are little-endian, but some older or specialized hardware (certain console devkits, some embedded platforms) uses big-endian. If you write a 32-bit integer as four bytes in little-endian on PC and read it as big-endian on another platform, the value is wrong and your game either crashes or loads corrupted state silently.
The safest approach: standardize on little-endian for all save files. On big-endian platforms, byte-swap every multi-byte value during read and write. Alternatively, use a self-describing format like MessagePack, Protocol Buffers, or FlatBuffers that handles endianness internally. The overhead is minimal and the portability gain is significant.
Even with text-based formats like JSON, watch for encoding issues. Ensure all save files are UTF-8 without BOM. Some platforms’ standard libraries default to UTF-16 or platform-specific encodings if you don’t specify.
Platform-Specific Serialization Quirks
Each platform has its own serialization surprises. Unity on iOS uses IL2CPP, which strips reflection metadata needed by some serializers (Newtonsoft.Json in particular). Add a link.xml to preserve the types you serialize. Switch has strict file I/O requirements — saves must go through the platform’s save data API, not raw file writes. PlayStation requires save data to be associated with a user account and mounted before access. Xbox uses Connected Storage, which is asynchronous and can fail if the user signs out mid-save.
Abstract your save I/O behind a platform interface. The serialization format and game state should be identical across platforms; only the storage layer changes. This lets you test the serialization logic on PC and only test the platform storage integration on each target device.
The Test Matrix
For N platforms, you need N × (N − 1) transfer tests: create a save on each platform, transfer to every other platform, load and verify. For three platforms (PC, console, mobile), that’s six test cases. For five platforms, it’s twenty. This grows fast, so automate as much as possible.
Create a “golden save” fixture for each platform. The save should exercise every serializable feature: maximum inventory, all quest states, edge-case character names (Unicode, emoji, maximum length), maximum play time, and every unlockable. Transfer each golden save to every other platform and verify with an automated check.
# verify_cross_save.py — CI verification step
import hashlib, json, sys
def verify(save_path, expected_checksum_path):
with open(save_path, "rb") as f:
data = f.read()
with open(expected_checksum_path) as f:
expected = json.load(f)
# Skip header (24 bytes), hash payload only
payload = data[24:]
actual_hash = hashlib.sha256(payload).hexdigest()
if actual_hash != expected["payload_sha256"]:
print(f"FAIL: expected {expected['payload_sha256']}")
print(f" got {actual_hash}")
sys.exit(1)
print("PASS: cross-save payload matches")
verify(sys.argv[1], sys.argv[2])
The verification loads the save, re-serializes the game state to memory, and compares a checksum against the expected value. If the round-trip produces the same bytes, the save transferred correctly. If not, the diff between the expected and actual payloads tells you exactly which field diverged.
Cloud Save as the Transfer Mechanism
Manual file transfer (USB, email, file share) is fine for testing but unreliable for players. Use a cloud save backend — your own, or a platform service like Steam Cloud, iCloud, or Google Play Games — as the transfer mechanism. The cloud backend normalizes the save format: the game uploads the save as a blob with metadata (version, platform, timestamp), and the target platform downloads and migrates it on load.
Test the cloud path separately from the serialization path. Cloud saves can fail due to quota limits, network timeouts, conflict resolution (two devices saving simultaneously), and authentication issues. Each of these needs its own test case. The most dangerous failure is silent data loss during conflict resolution — always prefer the save with the higher play time or more recent timestamp, and keep the losing save as a backup.
“A player who loses a 40-hour save file will never come back. Cross-save testing is not about polish — it is about trust. Get it wrong once and you lose that player forever.”
Regression Testing with Save Fixtures
Check a set of save fixtures into your repository: one per platform, one per historical schema version. Run a CI job that loads each fixture, migrates it to the current version, re-serializes, and verifies the checksum. This catches migration bugs before they reach players. When you add a new schema version, generate new fixtures on each platform and add them to the set.
Tag each fixture with the game version, platform, and a human-readable description of the game state it represents (“end of chapter 3, full inventory, level 42 character”). When a fixture test fails, the description tells you immediately what scenario broke.
Related Issues
If your cross-save issues involve achievement state not transferring, see How to Debug Achievement Unlock Failures. For general serialization testing approaches, check How to Build a Flaky Test Quarantine System for handling intermittent deserialization failures in CI.
Test the oldest save you have on the newest build. If it loads, your migration chain works. If it doesn’t, fix it before you ship.