Skip to content

Archived on 2026-10-24. This is a historical implementation note, not current setup instructions or an active task list. Original claims, code examples, file paths, and completion statements have not all been revalidated. Use the current documentation and issue register for maintenance decisions.

Newtonsoft.Json Implementation - Dictionary Serialization Solution

Overview

This document explains the implementation of Newtonsoft.Json (Json.NET) to handle Dictionary serialization in the Tiny-Chaos project, replacing Unity's limited JsonUtility.

The Problem

Unity's built-in JsonUtility cannot serialize Dictionary<TKey, TValue> types because: - Dictionaries use internal data structures that JsonUtility cannot access via reflection - JsonUtility only serializes public fields marked with [Serializable] - When you serialize PlayerData, all dictionary fields become null after deserialization

Example of the problem with JsonUtility:

PlayerData data = new PlayerData();
data.unlockedAchievement = new Dictionary<string, bool>();
data.unlockedAchievement["ACH_1"] = true;

string json = JsonUtility.ToJson(data);
// Result: {"version":"1.0.0", ...} 
// NOTE: unlockedAchievement field is MISSING from JSON!

PlayerData loaded = JsonUtility.FromJson<PlayerData>(json);
// Result: loaded.unlockedAchievement is NULL

The Solution

Newtonsoft.Json (Json.NET) provides native Dictionary support out of the box, making it simple and reliable.

What Was Changed

1. Added Newtonsoft.Json Package

  • Added via Unity Package Manager
  • Package: Newtonsoft.Json
  • Version: Latest stable (compatible with Unity)

2. Rewrote JsonUtils.cs

Replaced JsonUtility with Newtonsoft.Json wrapper:

using Newtonsoft.Json;

public static class JsonUtils
{
    public static string ToJson<T>(T obj, bool prettyPrint = false)
    {
        Formatting formatting = prettyPrint ? Formatting.Indented : Formatting.None;
        return JsonConvert.SerializeObject(obj, formatting);
    }

    public static T FromJson<T>(string json)
    {
        return JsonConvert.DeserializeObject<T>(json);
    }

    public static T[] GetArray<T>(string json)
    {
        return JsonConvert.DeserializeObject<T[]>(json);
    }
}

3. SaveManager.cs (No Changes Needed!)

SaveManager already uses JsonUtils.ToJson() and JsonUtils.FromJson(), so it works seamlessly with the new implementation.

JSON Structure Comparison

Before (Unity JsonUtility - Broken)

{
  "version": "1.0.0",
  "soulPoint": 100,
  "unlockedChar": {},          // Empty or missing
  "unlockedAchievement": {},    // Empty or missing
  "shopUpgrade": {}
}

After (Newtonsoft.Json - Working)

{
  "version": "1.0.0",
  "soulPoint": 100,
  "unlockedChar": {
    "SOLDIER": true,
    "KNIGHT": false
  },
  "unlockedAchievement": {
    "ACH_FIRST_WIN": true,
    "ACH_HIGH_SCORE": false
  },
  "shopUpgrade": {
    "UPG_DAMAGE": 3,
    "UPG_HEALTH": 1
  }
}

Note: Newtonsoft.Json serializes dictionaries as proper JSON objects (key-value pairs), which is cleaner and more standard.

Benefits of Newtonsoft.Json

1. Native Dictionary Support

  • ✅ Dictionary<TKey, TValue> works out of the box
  • ✅ No wrapper classes needed
  • ✅ No manual conversion code
  • ✅ Clean JSON output

2. No Maintenance Overhead

  • ✅ Automatically handles new fields in PlayerData
  • ✅ No need to update JsonUtils when adding fields
  • ✅ No need to manually maintain wrapper classes
  • ✅ Just add field to PlayerData and it works

3. Powerful Features

  • ✅ Supports complex nested types
  • ✅ Supports generic types
  • ✅ Supports attributes (JsonIgnore, JsonProperty, etc.)
  • ✅ Supports custom converters
  • ✅ Supports null handling options
  • ✅ Supports formatting options

4. Industry Standard

  • ✅ Widely used in .NET ecosystem
  • ✅ Well-documented
  • ✅ Active development and support
  • ✅ Battle-tested in production

5. Consistent API

  • ✅ JsonUtils wrapper provides consistent interface
  • ✅ SaveManager code unchanged
  • ✅ Minimal code changes required

Performance Considerations

Serialization Speed

  • Newtonsof.Json is generally faster than JsonUtility for complex objects
  • Dictionary handling is optimized
  • No reflection overhead for field conversion

Memory Usage

  • Similar to JsonUtility for simple types
  • More efficient for complex/dictionary types
  • Stream serialization available for large files

Build Size

  • Adds ~500KB to final build size
  • Single DLL assembly
  • No additional dependencies

Comparison with Alternatives

Approach Pros Cons
Newtonsoft.Json Native dictionary support, no maintenance, powerful, standard ~500KB build size
Unity JsonUtility Built-in, no dependencies Cannot serialize Dictionaries
Wrapper-based solution No external dependencies High maintenance, error-prone, manual updates needed
Manual fields Fastest Very verbose, hard to maintain

Usage Examples

Basic Serialization

PlayerData data = new PlayerData();
data.InitializeDefaults();
data.unlockedAchievement["TEST_ACH"] = true;

string json = JsonUtils.ToJson(data, prettyPrint: true);
Debug.Log(json);

Basic Deserialization

string json = File.ReadAllText("save.json");
PlayerData loaded = JsonUtils.FromJson<PlayerData>(json);

Debug.Log($"Achievements: {loaded.unlockedAchievement.Count}");
Debug.Log($"ACH_TEST_ACH: {loaded.unlockedAchievement["TEST_ACH"]}");

SaveManager Integration

// Save
public void SavePlayerData()
{
    Player.version = Application.version;
    string json = JsonUtils.ToJson(Player);
    string encrypted = EncryptionUtils.Encrypt(json);
    Game.Save.WriteNew(CURRENT_PLAYER_FILE, encrypted);
}

// Load
private void LoadNewEncryptedSave(string PLAYER_FILE)
{
    string encrypted = Game.Save.ReadNew(PLAYER_FILE);
    string json = EncryptionUtils.Decrypt(encrypted);
    Player = JsonUtils.FromJson<PlayerData>(json);
}

Installation Steps

1. Add Package

  1. Open Unity
  2. Window → Package Manager
  3. Click "+" → "Add package by name"
  4. Search: Newtonsoft.Json
  5. Click "Add" button
  6. Wait for package to install

2. Verify Installation

Check that Newtonsoft.Json appears in: - Project Settings → Player → Other Settings → Preloaded Assemblies - Or in Visual Studio references

3. Test Serialization

Run game and check console for JSON output with dictionaries properly serialized.

Testing

Test 1: Basic Serialization

PlayerData data = new PlayerData();
data.InitializeDefaults();
data.unlockedAchievement["TEST_ACH"] = true;

string json = JsonUtils.ToJson(data, prettyPrint: true);
Debug.Log(json);

// Verify JSON contains: "unlockedAchievement": { "TEST_ACH": true }
PlayerData loaded = JsonUtils.FromJson<PlayerData>(json);
Debug.Assert(loaded.unlockedAchievement["TEST_ACH"] == true);

Test 2: Empty Dictionary

PlayerData data = new PlayerData();
data.InitializeDefaults();

string json = JsonUtils.ToJson(data);
PlayerData loaded = JsonUtils.FromJson<PlayerData>(json);

Debug.Assert(loaded.unlockedAchievement != null);
Debug.Assert(loaded.unlockedAchievement.Count == 0);

Test 3: Save and Load Cycle

// Save
string json = JsonUtils.ToJson(Player);
string encrypted = EncryptionUtils.Encrypt(json);
Game.Save.WriteNew(FileConst.PLAYER_FILE, encrypted);

// Load
string encrypted = Game.Save.ReadNew(FileConst.PLAYER_FILE);
string json = EncryptionUtils.Decrypt(encrypted);
Player = JsonUtils.FromJson<PlayerData>(json);

Debug.Assert(Player.unlockedAchievement != null);

Test 4: Achievement Manager

// After loading save data
if (Player.unlockedAchievement != null)
{
    Debug.Log($"Achievements loaded: {Player.unlockedAchievement.Count}");

    foreach (var achievement in Player.unlockedAchievement)
    {
        Debug.Log($"  {achievement.Key}: {achievement.Value}");
    }
}
else
{
    Debug.LogError("unlockedAchievement is NULL!");
}

Troubleshooting

Issue: "The type or namespace name 'Newtonsoft' could not be found"

Cause: Newtonsoft.Json package not installed.

Solution: 1. Open Unity Package Manager 2. Install Newtonsoft.Json package 3. Restart Unity

Issue: Dictionaries are still null after loading

Cause: Not using JsonUtils or still using old JsonUtility code.

Solution: - Ensure all code uses JsonUtils.ToJson() and JsonUtils.FromJson() - Don't use JsonUtility.ToJson() or JsonUtility.FromJson()

Issue: JSON format different than expected

Cause: Newtonsoft.Json uses different formatting than JsonUtility.

Solution: - This is expected and correct behavior - Newtonsoft.Json serializes dictionaries as objects: {"key": value} - This is more standard than JsonUtility's array format

Issue: Build size too large

Cause: Newtonsoft.Json adds ~500KB to build.

Solution: - For mobile games, consider code stripping - For web builds, use IL2CPP code optimization - The 500KB is worth it for proper Dictionary support

Advanced Features

Custom Formatting

var settings = new JsonSerializerSettings
{
    Formatting = Formatting.Indented,
    NullValueHandling = NullValueHandling.Ignore,
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};

string json = JsonConvert.SerializeObject(obj, settings);

Custom Converters

public class Vector3Converter : JsonConverter<Vector3>
{
    public override void WriteJson(JsonWriter writer, Vector3 value, JsonSerializer serializer)
    {
        writer.WriteStartObject();
        writer.WritePropertyName("x");
        writer.WriteValue(value.x);
        writer.WritePropertyName("y");
        writer.WriteValue(value.y);
        writer.WritePropertyName("z");
        writer.WriteValue(value.z);
        writer.WriteEndObject();
    }

    public override Vector3 ReadJson(JsonReader reader, Type objectType, Vector3 existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        // Implement deserialization
    }
}

Attributes

[JsonObject(MemberSerialization.OptIn)]
public class PlayerData
{
    [JsonProperty("version")]
    public string version;

    [JsonProperty("soulPoint")]
    public int soulPoint;

    [JsonProperty("unlockedAchievement")]
    public Dictionary<string, bool> unlockedAchievement;

    [JsonIgnore]
    public string temporaryField;  // Won't be serialized
}

Migration from Old Solutions

From JsonUtility

Simply replace: - JsonUtility.ToJson(obj) → JsonUtils.ToJson(obj) - JsonUtility.FromJson<T>(json) → JsonUtils.FromJson<T>(json)

From Wrapper-Based Solution

Delete all wrapper classes: - Remove SerializableDictionaryStringBool - Remove SerializableDictionaryStringInt - Remove SerializationWrapper<T> - Simplify code significantly

Summary

Newtonsoft.Json implementation provides: - ✅ Native Dictionary support - Works out of the box - ✅ No external wrapper code - Clean, simple implementation - ✅ No maintenance - Automatic handling of new fields - ✅ Standard JSON format - Clean, readable output - ✅ Powerful features - Attributes, converters, settings - ✅ Industry standard - Well-documented, tested - ✅ Easy to use - Simple JsonUtils wrapper

This solution permanently fixes the NullReferenceException in AchievementManager by ensuring unlockedAchievement dictionary is properly serialized to and from JSON files.

  • EncryptionBypassGuide.md - Debug methods for manual JSON inspection
  • SaveDataDebuggerDocumentation.md - Comprehensive save data diagnostics

Support

For Newtonsoft.Json documentation: - Official docs: https://www.newtonsoft.com/json/help/html/Introduction.htm - GitHub: https://github.com/JamesNK/Newtonsoft.Json - Unity forums: Search for "Newtonsoft.Json Unity"