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¶
The Serialization system 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.