LoL Engine Serialization System¶
Overview¶
The Serialization System provides Newtonsoft JSON, obfuscated JSON, and encrypted JSON serialization for save data and other game-owned data models.
The public serializer contract is LoLEngine.Core.Serialization.Interfaces.IDataSerializer.
Features¶
- Standard JSON serialization through
JsonDataSerializer - Obfuscated JSON serialization through
ObfuscatedJsonDataSerializer(recommended for normal game saves) - Encrypted JSON serialization through
EncryptedJsonDataSerializer(for truly sensitive data) - Optional compression through
ICompressionService - Private Unity fields marked with
[SerializeField]are included throughUnityPrivateFieldContractResolver - Automatic serializer selection in
SaveSystem
Newtonsoft JSON Notes¶
LoL Engine uses Newtonsoft JSON, not Unity JsonUtility.
Supported¶
- Simple types, arrays, lists, dictionaries, structs, enums, and most POCO classes
- Public fields and properties
- Private fields marked with
[SerializeField]or[JsonProperty] DateTimeand other standard Newtonsoft-supported .NET types
Be Careful With¶
- Circular references unless you configure your own serializer strategy
- Interface or abstract fields unless you store type information yourself
- Unity object references (
GameObject,Transform,AudioClip, etc.); persist asset IDs, Addressable keys, or model IDs instead - Renaming fields after shipping saves; use migrations for live games
Unity Inspector Dictionaries¶
Newtonsoft can serialize dictionaries, but Unity's Inspector still cannot show a plain Dictionary<TKey, TValue>. Use SerializableDictionary<TKey, TValue> from LoLEngine.Helpers.Extensions only when you need Inspector editing.
using LoLEngine.Helpers.Extensions;
[Serializable]
public class StringIntDictionary : SerializableDictionary<string, int> { }
Service Registration¶
When Enable Serialization Service is on, ConfigurableServiceInitializer registers the base IDataSerializer and keyed variants:
| Key | Implementation | Availability |
|---|---|---|
default / json |
JsonDataSerializer |
Serialization service enabled |
obfuscated |
ObfuscatedJsonDataSerializer |
Obfuscation service registered |
encrypted |
EncryptedJsonDataSerializer |
Encryption service enabled and registered |
SaveSystem chooses the serializer from SaveSettings:
UseObfuscation = trueusesIDataSerializerkeyobfuscatedUseEncryption = trueusesIDataSerializerkeyencrypted- Both false uses
IDataSerializerkeyjson
Accessing Serializers¶
using LoLEngine.Core.Serialization.Interfaces;
using LoLEngine.Core.ServiceManagement.Service;
var jsonSerializer = ServiceLocator.Instance.Get<IDataSerializer>("json");
var obfuscatedSerializer = ServiceLocator.Instance.Get<IDataSerializer>("obfuscated");
var encryptedSerializer = ServiceLocator.Instance.Get<IDataSerializer>("encrypted");
In normal gameplay code, prefer using ISaveSystem instead of calling serializers directly.
Basic Usage¶
Standard JSON serialization:
var jsonSerializer = ServiceLocator.Instance.Get<IDataSerializer>("json");
PlayerData playerData = new PlayerData { Name = "Player1", Level = 10 };
string jsonData = jsonSerializer.Serialize(playerData, compress: false);
PlayerData loadedData = jsonSerializer.Deserialize<PlayerData>(jsonData, compress: false);
Obfuscated Serialization (Recommended)
// Get the obfuscated serializer
var obfuscatedSerializer = ServiceLocator.Instance.Get<IDataSerializer>("obfuscated");
// Serialize and obfuscate object (10x faster than encryption)
PlayerData playerData = new PlayerData { Name = "Player1", Level = 10 };
string obfuscatedData = obfuscatedSerializer.Serialize(playerData);
// Deobfuscate and deserialize object
PlayerData loadedData = obfuscatedSerializer.Deserialize<PlayerData>(obfuscatedData);
Encrypted Serialization (For Sensitive Data)
// Get the encrypted serializer
var encryptedSerializer = ServiceLocator.Instance.Get<IDataSerializer>("encrypted");
// Serialize and encrypt object (slower but more secure)
PlayerData playerData = new PlayerData { Name = "Player1", Level = 10 };
string encryptedData = encryptedSerializer.Serialize(playerData);
// Decrypt and deserialize object
PlayerData loadedData = encryptedSerializer.Deserialize<PlayerData>(encryptedData);
When to use each: - Obfuscated: Normal game saves, player progress, settings (fast, default) - Encrypted: Payment data, personal info, security-critical data (slow, secure) - Plain JSON: Development, debugging only (fastest, no protection)
Error Handling¶
try
{
var serializer = ServiceLocator.Instance.Get<IDataSerializer>("json");
string jsonData = serializer.Serialize(myData, compress: false);
}
catch (JsonSerializationException ex)
{
Debug.LogError($"JSON serialization failed: {ex.Message}");
}
catch (ServiceNotFoundException ex)
{
Debug.LogError("Serialization service not found");
}
Best Practices¶
- Data Model Design
- Use simple data types when possible
- Implement proper data contracts
-
Consider serialization attributes
-
Security
- Use obfuscated serialization for normal game data (default, recommended)
- Use encrypted serialization only for truly sensitive data (payment, personal info)
- Store encryption keys securely (never hardcode in production)
-
Validate deserialized data
-
Performance
- Prefer obfuscation over encryption for game saves (10x faster)
- Cache serialized results when appropriate
- Use plain JSON only for debugging
-
Handle large datasets carefully
-
Choosing the Right Serializer
- Obfuscated (.osav): Game progress, settings, inventory (fast, default)
- Encrypted (.esave): Payment data, passwords, personal info (slow, secure)
- Plain JSON (.sav): Development/debugging only (fastest, no protection)
See Data Persistence — Save protection for detailed comparison.
Example Data Model¶
public class PlayerData
{
public string Name { get; set; }
public int Level { get; set; }
public Dictionary<string, int> Inventory { get; set; }
}
Data Versioning and Migration (New in v1.1)¶
The Serialization system now supports data versioning to handle changes to your data structures over time.
When to Use Versioning¶
Use data versioning when: - You need to support old save files after updates - Your data structure will change over time - You want graceful migration instead of data loss - You're releasing updates to a live game
Basic Usage¶
Step 1: Wrap your data with VersionedData
using LoLEngine.Core.Serialization.Types;
// Your game data class
[Serializable]
public class PlayerData
{
public string playerName;
public int level;
public int gold;
}
// Save with version
var playerData = new PlayerData { playerName = "Hero", level = 10, gold = 1000 };
var versioned = VersionedData<PlayerData>.Create(playerData, version: 1);
// Serialize as normal
string json = jsonSerializer.Serialize(versioned);
Step 2: Load and migrate
// Deserialize
var versioned = jsonSerializer.Deserialize<VersionedData<PlayerData>>(json);
// Migrate if needed
int currentVersion = 2; // Your game is now on version 2
var migrated = DataVersioning.TryMigrate(versioned, currentVersion, MigratePlayerData);
// Migration function
private PlayerData MigratePlayerData(int oldVersion, PlayerData oldData)
{
if (oldVersion == 1)
{
// Version 1 -> 2: Added experience field
return new PlayerDataV2
{
playerName = oldData.playerName,
level = oldData.level,
gold = oldData.gold,
experience = oldData.level * 100 // Calculate from level
};
}
return oldData;
}
Advanced: Chain Multiple Migrations¶
For games with many versions, chain migrations together:
// Version 1: { name, level, gold }
// Version 2: { name, level, gold, experience }
// Version 3: { name, level, gold, experience, inventory }
private Func<int, PlayerData, PlayerData> CreateMigrator()
{
return DataVersioning.ChainMigrations<PlayerData>(
// Version 1 -> 2
(v, data) => {
var v2 = new PlayerDataV2();
// Copy old fields
v2.playerName = ((PlayerDataV1)data).playerName;
v2.level = ((PlayerDataV1)data).level;
v2.gold = ((PlayerDataV1)data).gold;
// Add new field
v2.experience = v2.level * 100;
return v2;
},
// Version 2 -> 3
(v, data) => {
var v3 = new PlayerDataV3();
// Copy all V2 fields
v3.playerName = ((PlayerDataV2)data).playerName;
v3.level = ((PlayerDataV2)data).level;
v3.gold = ((PlayerDataV2)data).gold;
v3.experience = ((PlayerDataV2)data).experience;
// Add new field
v3.inventory = new List<string>(); // Empty inventory
return v3;
}
);
}
// Use it
var migrated = DataVersioning.TryMigrate(versioned, currentVersion: 3, CreateMigrator());
Versioning Best Practices¶
-
Increment version for breaking changes only
// Breaking change - increment version public int level; // was: public string level; // Non-breaking addition - don't increment public int newField; // added to existing structure -
Always provide migration path
// Good: Handle all old versions private PlayerData Migrate(int oldVersion, PlayerData data) { if (oldVersion == 1) return MigrateV1ToV2(data); if (oldVersion == 2) return MigrateV2ToV3(data); return data; // Already current version } -
Test migrations with real data
[Test] public void TestMigration_V1ToV2() { // Create V1 data var v1 = new PlayerDataV1 { name = "Test", level = 5 }; var versioned = VersionedData<PlayerDataV1>.Create(v1, 1); // Migrate to V2 var v2 = DataVersioning.TryMigrate(versioned, 2, Migrator); // Verify migration Assert.AreEqual("Test", v2.name); Assert.AreEqual(5, v2.level); Assert.AreEqual(500, v2.experience); // Calculated field } -
Log migration events
// The system automatically logs migrations // [DataVersioning] Migrating data from version 1 to 2 // [DataVersioning] Successfully migrated data to version 2 -
Keep old versions for reference
// Don't delete old data classes - keep for migration [Serializable] public class PlayerDataV1 { /* ... */ } [Serializable] public class PlayerDataV2 { /* ... */ } // Current version [Serializable] public class PlayerData { /* ... */ }
Example: Complete Versioning Workflow¶
public class SaveSystem
{
private const int CURRENT_SAVE_VERSION = 3;
private IDataSerializer _serializer;
public void SaveGame(GameData data)
{
// Wrap with current version
var versioned = VersionedData<GameData>.Create(data, CURRENT_SAVE_VERSION);
// Serialize
string json = _serializer.Serialize(versioned);
File.WriteAllText(savePath, json);
}
public GameData LoadGame()
{
// Load and deserialize
string json = File.ReadAllText(savePath);
var versioned = _serializer.Deserialize<VersionedData<GameData>>(json);
// Migrate if needed
var migrated = DataVersioning.TryMigrate(
versioned,
CURRENT_SAVE_VERSION,
CreateGameDataMigrator()
);
if (migrated == null)
{
Debug.LogError("Failed to load save - migration failed");
return GetDefaultGameData();
}
return migrated;
}
private Func<int, GameData, GameData> CreateGameDataMigrator()
{
return DataVersioning.ChainMigrations<GameData>(
MigrateV1ToV2,
MigrateV2ToV3
);
}
}
Troubleshooting¶
Common issues and solutions:
- ServiceNotFoundException: Ensure ImprovedGameInitializer has run and the Serialization Service is enabled
- JsonSerializationException: Check data model compatibility
- CryptographicException / FormatException: Verify the encryption password/key is correct
- Invalid data: Ensure data hasn't been corrupted
- Memory issues: Consider chunking large datasets
- Migration failed: Check migration logic and version numbers
Version History¶
See CHANGELOG.md for release history.
License¶
The Serialization System is part of the LoL Engine framework and follows its licensing terms.