'Json.NET: how to remove assembly info from types in the resulting json string?

I am serializing using Json.NET, but the resulting string ends up way too long, because it includes a ton of surplus info about the assembly that I have no use for.

For example, here is what I'm getting for one of the types:

"Assets.Logic.CompGroundType, Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null": {
"$type": "Assets.Logic.CompGroundType, Assembly-CSharp",
"GroundType": 1,
"EntityID": 1,
"<GroundType>k__BackingField": 1
}

"GroundType" is an enum, and "EntityID" is int.

Here is my desired result:

"Assets.Logic.CompGroundType" : {
"$type": "Assets.Logic.CompGroundType",
"GroundType": 1,
"EntityID": 1,
"<GroundType>k__BackingField": 1
}

If it's possible, I would also like to remove the "$type" field while still correctly deserializing inherited types (I'm not sure why it's necessary, since that info is duplicated from one line above, but if I remove it by setting TypeNameHandling.None, I get deserialization errors for child types). I am also not sure what the last field (k__BackingField) is for.

If it's possible, I would want to reduce it even further, to:

"Assets.Logic.CompGroundType" : {
"GroundType": 1,
"EntityID": 1,
}

I understand it's possible to manually customize the serialization scheme for each type in Json.Net, but I have hundreds of types, and so I would like to do it automatically through some global setting.

I tried changing "FormatterAssemblyStyle", but there is no option for "None" there, only "Simple" or "Full", and I'm already using "Simple".

Thanks in advance for any help.

Edit:

It's important to note that the types are keys in a dictionary. That's why the type appears twice (in the first and second row of the first example).

After implementing a custom SerializationBinder, I was able to reduce the length of the "$type" field, but not the Dictionary key field. Now I get the following:

"componentDict": {
      "Assets.Logic.CompGroundType, Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null": {
        "$type": "Assets.Logic.CompGroundType",
        "GroundType": 1,
        "EntityID": 1,
        "<GroundType>k__BackingField": 1
      }
    }

Edit 2:

The code I'm trying to serialize is an entity component system. I'll try to provide a detailed example with code samples.

All components (including CompGroundType above) inherit from the following abstract class:

abstract class Component
{
    public int EntityID { get; private set; }
    protected Component(int EntityID)
    {
        this.EntityID = EntityID;
    }
}

The issue I'm encountering is in the serialization of the Entity class' componentDict:

class Entity
{
    readonly public int id;

    private Dictionary<Type, Component> componentDict = new Dictionary<Type, Component>();

    [JsonConstructor]
    private Entity(Dictionary<Type, Component> componentDict, int id)
    {
        this.id = id;
        this.componentDict = componentDict;
    }
}

componentDict contains all the components attached to the entity. In each entry <Type, Component>, the type of the value is equal to the key.

I am doing the serialization recursively, using the following JsonSerializerSettings:

JsonSerializerSettings serializerSettings = new JsonSerializerSettings()
{
     ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
     ContractResolver = new MyContractResolver(),
     TypeNameHandling = TypeNameHandling.Auto,
     SerializationBinder = new TypesWithNoAssmeblyInfoBinder(),
     Formatting = Formatting.Indented
}

Where MyContractResolver is identical to the one form this answer.

TypesWithNoAssmeblyInfoBinder does the change from edit 1:

private class TypesWithNoAssmeblyInfoBinder : ISerializationBinder
        {
            public Type BindToType(string assemblyName, string typeName)
            {
                return Type.GetType(typeName);
            }

            public void BindToName(Type serializedType, out string assemblyName, out string typeName)
            {
                assemblyName = null;
                typeName = serializedType.FullName;
            }
        }

The serialization itself is done as follows:

            var jsonSerializer = JsonSerializer.Create(serializerSettings);

            using (FileStream zippedFile = new FileStream(Application.persistentDataPath + fileName, FileMode.Create))
            {
                using (GZipStream archive = new GZipStream(zippedFile, CompressionLevel.Fastest))
                {
                    using (StreamWriter sw = new StreamWriter(archive))
                    {
                        jsonSerializer.Serialize(sw, savedData);
                    }
                }
            }

Edit 4:

The CompGroundType class (an example of a finished component):

class CompGroundType : Component
{
    public enum Type {Grass, Rock};

    public Type GroundType { get; private set; }

    [JsonConstructor]
    private CompGroundType(Type groundType, int entityID) : base(entityID)
    {
        this.GroundType = groundType;
    }
}


Solution 1:[1]

The first part is the embedded $type information which is being injected by json.net to help with deserialization later. I think this example from the documentation will do what you want.

public class KnownTypesBinder : ISerializationBinder
{
    public IList<Type> KnownTypes { get; set; }

    public Type BindToType(string assemblyName, string typeName)
    {
        return KnownTypes.SingleOrDefault(t => t.Name == typeName);
    }

    public void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        assemblyName = null;
        typeName = serializedType.Name;
    }
}

public class Car
{
    public string Maker { get; set; }
    public string Model { get; set; }
}

KnownTypesBinder knownTypesBinder = new KnownTypesBinder
{
    KnownTypes = new List<Type> { typeof(Car) }
};

Car car = new Car
{
    Maker = "Ford",
    Model = "Explorer"
};

string json = JsonConvert.SerializeObject(car, Formatting.Indented, new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Objects,
    SerializationBinder = knownTypesBinder
});

Console.WriteLine(json);
// {
//   "$type": "Car",
//   "Maker": "Ford",
//   "Model": "Explorer"
// }

object newValue = JsonConvert.DeserializeObject(json, new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Objects,
    SerializationBinder = knownTypesBinder
});

Console.WriteLine(newValue.GetType().Name);
// Car

It just needs tweaking to your particular needs.

Then the second part is dictionary key, which is coming from Type objects being serialized.

I thought you can customize that by creating a custom JsonConverter, but it turns out that dictionary keys have "special" handling, meaning a more involved workaround. I don't have an example of the more involved workaround sorry.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1