4

I am developing a game which has a class called UserInfo.

Inside the UserInfo class there is a public List<Lineup> lineups variable.

And Lineup contains public Dictionary<Vector2Int, Collection> cardLocations.

My problem is, JsonConvert.SerializeObject does correctly produce the result I want, but when I do JsonConvert.DeserializeObject<UserInfo>, Vector2Int remains to be a string like "(x, y)".

How should I fix it?

I wrote a JsonConverter to convert it, and used

JsonConvert.SerializeObject(userInfo, Formatting.Indented, new Vec2Converter()) 

for serialization and

JsonConvert.DeserializeObject<UserInfo>(json, new Vec2Converter())

to deserialization, but it doesn't work.

Here is my JsonConverter script:

using Newtonsoft.Json;
using UnityEngine;
using System;

public class Vec2Converter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteValue(value.ToString());
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var vector2Int = serializer.Deserialize<Vector2Int>(reader);
        Debug.Log(vector2Int);
        return vector2Int;
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Vector2Int);
    }
}

Another weird thing is that, when I tried to change Vector2Int to other data structure like Lineup or Dictionary<Vector2Int, Collection>, Unity exited when I clicked play. After I changed it back, it became ok. The ok means it's not exiting but still giving me the error message.

Forgot to put the error message: ArgumentException: Could not cast or convert from System.String to UnityEngine.Vector2Int

Here are the classes.

public class UserInfo {
    public string username;
    public int playerID;
    public List<Collection> collection = new List<Collection>();
    public List<Lineup> lineups = new List<Lineup>(); // Here is the problem
    public Dictionary<string, int> contracts = new Dictionary<string, int>();
    public int coins = 0;
    public int rank = 0;
    public int lastLineupSelected = -1;
    public int winsToday = 0;
    public Stats total = new Stats();
    public Dictionary<string, Stats> boardResults = new Dictionary<string, Stats>();
    public List<Mission> missions;
    public string preferredBoard = "Standard Board";
    public string lastModeSelected = "";
    public int gameID;
    public bool missionSwitched = false;
}

public class Lineup
{
    public Dictionary<Vector2Int, Collection> cardLocations;  // Here is the problem
    public List<Tactic> tactics;
    public string boardName;
    public string lineupName;
    public string general;
    public bool complete;
}

public class Collection
{
    public string name = "";
    public string type = "";
    public int count = 1;
    public int health = 0;
    public int oreCost = 0;
}
Seaky
  • 239
  • 2
  • 13
  • Then manually parse the string and create the desired type. – Nkosi May 26 '18 at 01:41
  • @Nkosi Could you be more specific? How can I create the desired type? – Seaky May 26 '18 at 01:47
  • 1
    Seems dictionary keys are handled specially and need to convertible to strings. See [Not able To Serialize Dictionary with Complex key using Json.net](https://stackoverflow.com/q/24504245) and [How can I serialize/deserialize a dictionary with custom keys using Json.Net?](https://stackoverflow.com/q/24681873) and [json.net: specify converter for dictionary keys](https://stackoverflow.com/q/6845364). – dbc May 26 '18 at 03:25
  • @dbc My case is a little bit different. They are just converting dictionary but I am doing a data structure that has a data structure containing a dictionary. And Vector2Int is a unity builtin data type which I can't modify. – Seaky May 26 '18 at 04:03
  • You have to post the class(UserInfo) you are trying to serialize. You haven't done so. You also have to show how you initialized the variable. – Programmer May 26 '18 at 04:54
  • @Programmer posted. – Seaky May 26 '18 at 05:09
  • You said Vector2Int is not serializing/de-serializing but I don't even see Vector2Int there. – Programmer May 26 '18 at 05:15
  • @Programmer Sorry I was trying a new solution so I put Location there by mistake instead of Vector2Int. I have fixed it. Please check it out. Sorry for the trouble. Serializing is fine. I just have trouble deserializing it. – Seaky May 26 '18 at 06:13
  • Where is `Collection` from? Which namespace or is a class you made? If you made it then post the code since it's related to the issue too. – Programmer May 26 '18 at 06:23
  • @Programmer Sorry for the confusion. I made Collection and it is posted now. – Seaky May 26 '18 at 06:42

3 Answers3

3

Your custom serializer that derives from JsonConverter needs more work and there are few errors. First, note that you are trying to serialize/de-serialize Dictionary of Vector2Int not just a Vector2Int variable.

It's this:

public Dictionary<Vector2Int, Collection> cardLocations.

not

public Vector2Int cardLocations;

Because of the above statement, your override bool CanConvert(Type objectType) function should be checking for typeof(Dictionary<Vector2Int, Collection>) not typeof(Vector2Int). Also, you also need to add checks to determine when to run the custom de-serializer.

Serializing:

1.In the WriteJson function that serializes the json, you only need to customize the Dictionary<Vector2Int, Collection> part which is throwing the exception so make sure that the custom code runs only when the type is Dictionary<Vector2Int, Collection>. You can do this by putting the code inside if (value is Dictionary<Vector2Int, Collection>).

2.Obtain the Dictionary to serialize from the second argument. Call writer.WriteStartArray() so that you'll write every value as an array. Now, loop over the Dictionary, write the key with writer.WriteValue(key) then write the value with serializer.Serialize(writer, entry.Value);.

3. After/outside the loop call writer.WriteStartArray(); then return from that function.

If the if (value is Dictionary<Vector2Int, Collection>) from #1 is false then simply call writer.WriteStartObject() followed by writer.WriteEndObject().


De-Serializing:

4.Determine when to run the custom de-serializer

We used if (value is Dictionary<Vector2Int, Collection>) in #1 when serializing to determine if the custom serializer code should run but for de-serializing, we use if (reader.TokenType == JsonToken.StartArray) to determine if we have reached the array part where we did our custom serialization in the ReadJson function.

5.Use JArray.Load(reader); to obtain the array data we serialized. In the returned array, the first element in it is the key in the Dictionary.The second element is the value in the Dictionary. The third element is the second key in the Dictionary. The fourth element is the second value in the Dictionary ans so on.

6.Separate the keys and values in the JArray.

To simplify this, loop over the JArray increment by 2 instead of 1. By incrementing by 2, the key can be easily be retrieved with JArray[loop + 0] and the value can be retrieved in the-same loop with JArray[loop + 1].

for (int i = 0; i < jArray.Count; i += 2)
{
    //Key
    string key = jArray[i + 0].ToString();
    //Value
    string value = jArray[i + 1].ToString();
}

7.Get the key in Vector2Int format.

Simply de-serialize the key from #6 into Vector2Int with JsonConvert.DeserializeObject<Vector2Int>(key).

8.Get the value in Collection format.

Simply de-serialize the value from #6 into Collection with JsonConvert.DeserializeObject<Collection>(value).

9.Reconstruct the data

Create new instance of Dictionary<Vector2Int, Collection> then add both the key from #7 and value from #8 to it and then return it from the ReadJson function.

If the if (reader.TokenType == JsonToken.StartArray) from #4 is false, simply create and return new instance of Dictionary<Vector2Int, Collection> from the ReadJson function without adding key or value to it.

class Vec2DictionaryConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(Dictionary<Vector2Int, Collection>).IsAssignableFrom(objectType);
    }

    //Deserialize json to an Object
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        //Debug.Log("De-serializing!");
        if (reader.TokenType == JsonToken.StartArray)
        {
            // Load JArray from stream
            JArray jArray = JArray.Load(reader);

            //Where to re-create the json data into 
            Dictionary<Vector2Int, Collection> dict = new Dictionary<Vector2Int, Collection>();

            if (jArray == null || jArray.Count < 2)
            {
                return dict;
            }

            //Do the loop faster with +=2
            for (int i = 0; i < jArray.Count; i += 2)
            {
                //first item = key
                string firstData = jArray[i + 0].ToString();
                //second item = value
                string secondData = jArray[i + 1].ToString();

                //Create Vector2Int key data 
                Vector2Int vect = JsonConvert.DeserializeObject<Vector2Int>(firstData);

                //Create Collection value data
                Collection values = JsonConvert.DeserializeObject<Collection>(secondData);

                //Add both Key and Value to the Dictionary if key doesnt exit yet
                if (!dict.ContainsKey(vect))
                    dict.Add(vect, values);
            }
            //Return the Dictionary result
            return dict;
        }
        return new Dictionary<Vector2Int, Collection>();
    }

    //SerializeObject to Json
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        //Debug.Log("Serializing!");
        if (value is Dictionary<Vector2Int, Collection>)
        {
            //Get the Data to serialize
            Dictionary<Vector2Int, Collection> dict = (Dictionary<Vector2Int, Collection>)value;

            //Loop over the Dictionary array and write each one
            writer.WriteStartArray();
            foreach (KeyValuePair<Vector2Int, Collection> entry in dict)
            {
                //Write Key (Vector) 
                serializer.Serialize(writer, entry.Key);
                //Write Value (Collection)
                serializer.Serialize(writer, entry.Value);
            }
            writer.WriteEndArray();
            return;
        }
        writer.WriteStartObject();
        writer.WriteEndObject();
    }
}

Usage:

Serialize

string json = JsonConvert.SerializeObject(userInfo, new Vec2DictionaryConverter());

De-serialize

UserInfo obj = JsonConvert.DeserializeObject<UserInfo>(json, new Vec2DictionaryConverter());
Programmer
  • 121,791
  • 22
  • 236
  • 328
0

Manually parse the string and create the desired type.

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
    var vector2IntString = reader.Value.ToString();//expecting "(x, y)"
    Debug.Log(vector2IntString);
    var parts = vector2IntString.Split(new char[]{ '(', ')', ',', ' '}, StringSplitOptions.RemoveEmptyEntries);
    var x = int.Parse(parts[0]);
    var y = int.Parse(parts[1]); 
    var vector2Int = new Vector2Int(x, y);
    return vector2Int;
}

The above takes the "(x, y)" string and extracts the x and y values. It converts them to integers and uses them to initialize an instance of the desired type (Vector2Int)

Some further reading about this particular scenario revealed

If you only need a type conversion for JSON, you can also use JSON.NET's own custom type converters. Although this doesn't work in the case of dictionary keys.

Lastly, if you have a complex type as your dictionary key, you may want to consider just serializing it as a collection and then deserializing it using a JSON.NET custom converter.

Community
  • 1
  • 1
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • Thank you but I just tried your code, it's still not working. I think my problem isn't not creating a correct Vector2Int. I guess I should use a converter like lineupConverter or dictConverter? But that's too complicated since I have a lot of variables. – Seaky May 26 '18 at 02:14
  • @Seaky explain what you mean by `not working`. what is happening? – Nkosi May 26 '18 at 02:15
  • I got ArgumentException: Could not cast or convert from System.String to UnityEngine.Vector2Int before and after trying your code. – Seaky May 26 '18 at 02:17
  • @Seaky On which line? – Nkosi May 26 '18 at 02:20
  • It doesn't tell me. I think the script and the code are correct but could it be that the Vector2Int is so inside the data structure that it is not detected? – Seaky May 26 '18 at 02:25
  • Yeah my problem is exactly how to deserialize it. I wrote my solution but it doesn't work. – Seaky May 26 '18 at 03:37
0

For somebody like me that will run into the same issue.

@Programmer's answer is correct, but may be outdated (I don't know how Json.NET looked like in 2018) and it's too much code (at least for me).

Here's my solution:

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

...

[JsonConverter(typeof(Vec2DictionaryConverter))]
public Dictionary<Vector2Int, Collection> cardLocations;

...

public class DictionaryConverter : JsonConverter<Dictionary<Vector2Int, Collection>> {

    private static readonly string[] SEPARATORS = new[] { "(", ", ", ")" };

    public override Dictionary<Vector2Int, Value> ReadJson(JsonReader reader, Type objectType,
            Dictionary<Vector2Int, Collection> existingValue, bool hasExistingValue, JsonSerializer serializer) {
        if (JsonToken.Null == reader.TokenType) {
            return null;
        }
        var dictionary = new Dictionary<Vector2Int, Collection>();
        foreach (var pair in JObject.Load(reader)) {
            var vector = pair.Key.Split(SEPARATORS, StringSplitOptions.RemoveEmptyEntries)
                .Select(it => Convert.ToInt32(it));
            var key = new Vector2Int(vector.First(), vector.Last());
            var value = pair.Value.ToObject<Collection>(serializer);
            dictionary.Add(key, value);
        }
        return dictionary;
    }

    public override void WriteJson(JsonWriter writer,
            Dictionary<Vector2Int, Collection> value, JsonSerializer serializer) {
        serializer.Serialize(writer, value);
    }
}
  1. JsonConverter Attribute is used instead of passing Converter to SerializeObject/DeserializeObject all the time
  2. Generic JsonConverter<Dictionary<Vector2Int, Collection>> is used, so no need to implement CanConvert anymore
  3. Only ReadJson is implemented as OP didn't have problems with Serialization

This works fine and dandy, although we rely on Vector2Int.ToString implementation, but as I found out Unity's JsonUtility serialization is faster at least in my tests.

RunninglVlan
  • 170
  • 2
  • 13