I am working on a unity project where you can save a Dictionary that has key of custom type GraphNode and the values of each key are a list of the same type GraphNode Dictionary<GraphNode, List<GraphNode>>
. I am using a NewtonSoft library for json converting from objects to json text and converting from text to objects.
GameData script:
using System;
using System.Collections.Generic;
[Serializable]
public class GameData
{
public Dictionary<GraphNode, List<GraphNode>> allGraphNodes;
public GameData()
{
allGraphNodes = new Dictionary<GraphNode, List<GraphNode>>();
}
}
GraphNode class:
public class GraphNode
{
public enum Type
{
entrance = 0,
enemy = 1,
hub = 2,
shop = 3,
boss = 4
}
public Type type;
public GraphNode(Type type2)
{
SetType(type2);
}
public void SetType(Type type2)
{
type = type2;
}
}
I tried similar examples on stackoverflow but it didn't work, this is for saving:
public void Save(GameData data)
{
string text = Path.Combine(dataDirPath, dataFileName);
try
{
Directory.CreateDirectory(Path.GetDirectoryName(text));
string text3 = JsonConvert.SerializeObject(data, Formatting.Indented, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All,
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple
});
if (useEncryption)
{
text3 = EncryptDecrypt(text3);
}
using FileStream stream = new FileStream(text, FileMode.Create);
using StreamWriter streamWriter = new StreamWriter(stream);
streamWriter.Write(text3);
}
catch (Exception ex)
{
Debug.LogError("Error" + text + "\n" + ex);
}
}
And saved example of dictionary looks like this:
{
"$type": "GameData, Assembly-CSharp",
"allGraphNodes": {
"$type": "System.Collections.Generic.Dictionary`2[[GraphNode, Assembly-CSharp],[System.Collections.Generic.List`1[[GraphNode, Assembly-CSharp]], mscorlib]], mscorlib",
"GraphNode": {
"$type": "System.Collections.Generic.List`1[[GraphNode, Assembly-CSharp]], mscorlib",
"$values": [
{
"$type": "GraphNode, Assembly-CSharp",
"type": 1
}
]
},
"GraphNode": {
"$type": "System.Collections.Generic.List`1[[GraphNode, Assembly-CSharp]], mscorlib",
"$values": [
{
"$type": "GraphNode, Assembly-CSharp",
"type": 0
},
{
"$type": "GraphNode, Assembly-CSharp",
"type": 3
},
{
"$type": "GraphNode, Assembly-CSharp",
"type": 1
}
]
},
"GraphNode": {
"$type": "System.Collections.Generic.List`1[[GraphNode, Assembly-CSharp]], mscorlib",
"$values": [
{
"$type": "GraphNode, Assembly-CSharp",
"type": 2
},
{
"$type": "GraphNode, Assembly-CSharp",
"type": 1
}
]
},
"GraphNode": {
"$type": "System.Collections.Generic.List`1[[GraphNode, Assembly-CSharp]], mscorlib",
"$values": [
{
"$type": "GraphNode, Assembly-CSharp",
"type": 1
},
{
"$type": "GraphNode, Assembly-CSharp",
"type": 1
},
{
"$type": "GraphNode, Assembly-CSharp",
"type": 1
}
]
},
"GraphNode": {
"$type": "System.Collections.Generic.List`1[[GraphNode, Assembly-CSharp]], mscorlib",
"$values": [
{
"$type": "GraphNode, Assembly-CSharp",
"type": 3
},
{
"$type": "GraphNode, Assembly-CSharp",
"type": 2
}
]
},
"GraphNode": {
"$type": "System.Collections.Generic.List`1[[GraphNode, Assembly-CSharp]], mscorlib",
"$values": [
{
"$type": "GraphNode, Assembly-CSharp",
"type": 1
},
{
"$type": "GraphNode, Assembly-CSharp",
"type": 1
}
]
},
"GraphNode": {
"$type": "System.Collections.Generic.List`1[[GraphNode, Assembly-CSharp]], mscorlib",
"$values": [
{
"$type": "GraphNode, Assembly-CSharp",
"type": 2
},
{
"$type": "GraphNode, Assembly-CSharp",
"type": 4
},
{
"$type": "GraphNode, Assembly-CSharp",
"type": 2
}
]
},
"GraphNode": {
"$type": "System.Collections.Generic.List`1[[GraphNode, Assembly-CSharp]], mscorlib",
"$values": [
{
"$type": "GraphNode, Assembly-CSharp",
"type": 1
}
]
},
"GraphNode": {
"$type": "System.Collections.Generic.List`1[[GraphNode, Assembly-CSharp]], mscorlib",
"$values": [
{
"$type": "GraphNode, Assembly-CSharp",
"type": 1
},
{
"$type": "GraphNode, Assembly-CSharp",
"type": 4
},
{
"$type": "GraphNode, Assembly-CSharp",
"type": 1
},
{
"$type": "GraphNode, Assembly-CSharp",
"type": 3
}
]
},
"GraphNode": {
"$type": "System.Collections.Generic.List`1[[GraphNode, Assembly-CSharp]], mscorlib",
"$values": [
{
"$type": "GraphNode, Assembly-CSharp",
"type": 2
},
{
"$type": "GraphNode, Assembly-CSharp",
"type": 3
}
]
},
"GraphNode": {
"$type": "System.Collections.Generic.List`1[[GraphNode, Assembly-CSharp]], mscorlib",
"$values": [
{
"$type": "GraphNode, Assembly-CSharp",
"type": 2
}
]
},
"GraphNode": {
"$type": "System.Collections.Generic.List`1[[GraphNode, Assembly-CSharp]], mscorlib",
"$values": [
{
"$type": "GraphNode, Assembly-CSharp",
"type": 2
},
{
"$type": "GraphNode, Assembly-CSharp",
"type": 3
}
]
},
"GraphNode": {
"$type": "System.Collections.Generic.List`1[[GraphNode, Assembly-CSharp]], mscorlib",
"$values": [
{
"$type": "GraphNode, Assembly-CSharp",
"type": 1
},
{
"$type": "GraphNode, Assembly-CSharp",
"type": 4
}
]
}
}
}
And this for loading (part where the error comes is marked as an important line):
public GameData LoadLayout()
{
string text = Path.Combine(dataDirPath, dataFileName);
GameData result = null;
if (File.Exists(text))
{
try
{
string text2 = "";
using (FileStream stream = new FileStream(text, FileMode.Open))
{
using StreamReader streamReader = new StreamReader(stream);
text2 = streamReader.ReadToEnd();
Debug.Log(text2);
}
if (useEncryption)
{
text2 = EncryptDecrypt(text2);
}
result = JsonConvert.DeserializeObject<GameData>(text2, new JsonSerializerSettings //important line
{
TypeNameHandling = TypeNameHandling.All,
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple
});
return result;
}
catch (Exception ex)
{
Debug.LogError("Error" + text + "\n" + ex);
return result;
}
}
return result;
}
Error print:
Newtonsoft.Json.JsonSerializationException: Could not convert string 'GraphNode' to dictionary key type 'GraphNode'. Create a TypeConverter to convert from the string to the key type object. Path 'allGraphNodes.GraphNode', line 5, position 16. ---> Newtonsoft.Json.JsonSerializationException: Error converting value "GraphNode" to type 'GraphNode'. Path 'allGraphNodes.GraphNode', line 5, position 16. ---> System.ArgumentException: Could not cast or convert from System.String to GraphNode.
You are encountering several problems:
Your dictionary has complex keys, however, as explained in the docs, since Json.NET serializes dictionaries as JSON objects by default, serialization only works for keys that are convertible to strings.
Thus you will need to serialize your allGraphNodes
as an array. There are several ways to do this shown in Serialize dictionary as array (of key value pairs) but the simplest is probably to serialize the dictionary using a surrogate array property.
The List<GraphNode>
values in your dictionary presumably refer to other keys in the dictionary. To preserve these references across serialization you need to serialize with reference preservation enabled.
Unfortunately reference preservation does not work with parameterized constructors, so GraphNode
will need a parameterless constructor. It can be private as long as you mark it with [JsonConstructor]
.
You are serializing with TypeNameHandling.All
however as explained in the documentation this introduces security risks. For details see TypeNameHandling caution in Newtonsoft Json and External json vulnerable because of Json.Net TypeNameHandling auto?. It can also cause unnecessary bloat in your JSON files.
Your current data model does not need this setting because it does not use polymorphism. If you do find it necessary, consider writing a custom serialization binder or using it only for specific polymorphic properties.
Putting all this together, if you modify your data model as follows:
[Serializable]
public class GameData
{
[JsonIgnore]
public Dictionary<GraphNode, List<GraphNode>> allGraphNodes;
public GameData()
{
allGraphNodes = new Dictionary<GraphNode, List<GraphNode>>();
}
[JsonProperty("allGraphNodes")]
KeyValuePair<GraphNode, List<GraphNode>> [] allGraphNodeArray
{
get { return allGraphNodes?.ToArray(); }
set
{
allGraphNodes = allGraphNodes ?? new Dictionary<GraphNode, List<GraphNode>>();
if (value != null)
foreach (var pair in value)
allGraphNodes.Add(pair.Key, pair.Value);
}
}
}
[Serializable]
[JsonObject(IsReference = true)] // Forces all instances to be serialized with reference preservation
public class GraphNode
{
public enum Type
{
entrance = 0,
enemy = 1,
hub = 2,
shop = 3,
boss = 4
}
public Type type;
[JsonConstructor] // Constructor for serialization.
GraphNode() { }
public GraphNode(Type type) { SetType(type); }
public void SetType(Type type) { this.type = type; }
}
You will be able to serialize as follows:
var settings = new JsonSerializerSettings
{
// Add any settings as required, e.g.
// Converters = { new StringEnumConverter() },
};
var json1 = JsonConvert.SerializeObject(gameData1, settings);
var gameData2 = JsonConvert.DeserializeObject<GameData>(json1, settings);
Demo fiddle here.