Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Could not determine JSON object type for type System.Collections.Generic.KeyValuePair #2961

Open
Draco18s opened this issue Jun 24, 2024 · 1 comment

Comments

@Draco18s
Copy link

Source type

Source type: Dictionary<int, AbstractCustomType> where AbstractCustomType has concrete subclasses that each have a custom contract resolver implemented. Adding a JsonConverter for the abstract type did not help. Using the Newtonsoft Json for Unity version 3.2.1

public abstract class AbstractCustomType {
    //defines several abstract methods
}

public class ConcreteImplA : AbstractCustomType {
    public int serializableValue;
    //implements those abstract methods
}

public class ConcreteImplAResolver : JsonConverter {
	public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
	{
		ConcreteImplA v = (ConcreteImplA)value;
		JObject o = new JObject();
		o.Add(new JProperty("class", v.GetType().AssemblyQualifiedName));
		o.Add(new JProperty("value", v.serializableValue));
		o.WriteTo(writer);
	}

	public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
	{
		JObject jObject = JObject.Load(reader);
		string id = (string)jObject.GetValue("class");
		ConcreteImplA runObj = Activator.CreateInstance(Type.GetType(id));
		runObj.serializableValue= (int)jObject.GetValue("value");
		return runObj;
	}

	public override bool CanConvert(Type objectType)
	{
		return typeof(ConcreteImplA).IsAssignableFrom(objectType);
	}
}

Expected behavior

This works:

JsonSerializerSettings settings = new JsonSerializerSettings();
settings.ContractResolver = new ContractResolver();
ContractResolver.jsonSettings = settings;
Dictionary<int, AbstractCustomType> dict = new Dictionary<int, AbstractCustomType>
{
	{0, new ConcreteImplA()}
};

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

Producing this json:

{
    "0": {
        "class": "ConcreteImplA",
        "value": 0
    }
}

However, when the concrete implementation contains another object (that also has a converter) that then contains a dictionary of these types, it fails:

public class ConcreteImplB : AbstractCustomType {
    public ChildGroup grouping = new ChildGroup();
}
// with defined JsonConverter specified in the ContractResolver

public class ChildGroup {
    public Dictionary<int, AbstractCustomType> childObjects; //this dictionary is the source of the error
    public ChildGroup() {
       childObjects = new Dictionary<int, AbstractCustomType>
       {
           {0, new ConcreteImplA()
           {
               serializableValue = 1
           }}
       };
    }
}
// with defined JsonConverter specified in the ContractResolver

Expected JSON:

{
    "0": {
        "class": "ConcreteImplB",
        "grouping": {
            "childObjects": {
                "0": {
                    "class": "ConcreteImplA",
                    "value": 1
                }
            }
        }
    }
}

Actual behavior

ArgumentException: Could not determine JSON object type for type System.Collections.Generic.KeyValuePair2[System.Int32,AbstractCustomType].`

Steps to reproduce

JsonSerializerSettings settings = new JsonSerializerSettings();
settings.ContractResolver = new ContractResolver();
ContractResolver.jsonSettings = settings;
Dictionary<int, AbstractCustomType> dict = new Dictionary<int, AbstractCustomType>
{
	{0, new ConcreteImplB()}
};

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

Additional details & Repo

I use a custom Attribute to create/resolve the converters, which shouldn't impact the behavior, it just lets me more quickly handle them and don't have to constantly update the ContractResolver class and instead use the attribute on the class itself and make the Converter a contained child class (which can access private/protected fields):

[JsonResolver(typeof(Converter))]
public class ConcreteImplC : AbstractCustomType {
    // ...
    public class Converter : JsonConverter {
        // ...
    }
}

(If you want to yoink that attribute and means of registering converters and add it to the package, by all means, go for it; I did it that way to make my life easier and comparable method didn't exist or I couldn't find it. While you're at it, fix the existing converters for Unity structs, it keeps trying to deserialize the read-only property magnitude on things like Vector3).

The inheritance of my classes are also rather involved, as there's the runtime object which takes properties from a serializable object, but which can be modified to be different than the base serialized version (think Minecraft Items, there's the Item which defines what a stick is, then there's the ItemStack which represents this specific stick: the two serialize differently as the former is an asset that may be referenced hundreds of times by the instances and the later is a runtime mutable instance derived from the asset which has its own modified values). Some of these modifiable properties are collections containing more mutable instances ("this is a Bag of Sticks and Stones, there's a stick in slot 0, a stone in slot 1, slot 2 is empty...").

So I'm not actually using reflection to create the new instances, but rather looking up in the ItemRegistry the Asset by its name-id and asking it for a new instance, but it functions similarly to CreateInstance(GetType(...)). Entire project in its current state is available. The json call is here. The first SerializeObject call on 70 works, the one on 73 does not.

(If you clone the repo to run this in Unity yourself, I'm using Unity 2022.3.22, the #if on 43 needs to be inverted to simulate non-editor behavior, then enter play mode in the Main scene (it should open with that scene active, otherwise it is in Assets/Scenes/Main), click the thing shooting bullets, add anything that creates more bullets to the timeline (Split Shot, Death Blossom, 60 Degree Spray are all in the card pool, which one shows up, if any, is going to be random), then click Save Asset and it will print the string in the console).

@Draco18s
Copy link
Author

Draco18s commented Jun 24, 2024

Oh I should note that in the converters if I convert the dictionary to a json string, then add that json string as the JProperty, everything's great.

I just end up with backslashes to end all other text due to the repeated string to json-safe-string conversion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant