7

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;
    }
}
user92748
  • 71
  • 1
  • 3
  • One option would be to use `DefaultAssemblyBinder` from [Json serialization for Object Data Type](https://stackoverflow.com/a/34581537/3744182). Another would be `SimpleAssemblyMappingSerializationBinder` from [JsonConverter how to deserialize to generic object](https://stackoverflow.com/a/35349393/3744182). – dbc Nov 28 '17 at 18:44
  • I have implemented custom binders like your examples suggested, but it seems that it was only a partial solution. Please see my edit. – user92748 Nov 28 '17 at 21:40
  • I think we may need to see a [mcve] to help further. Can you share c# classes that reproduce the output you are using using the full Json.NET library? Is `componentDict` a `Dictionary` where `T` is guaranteed to be of the same type as the dictionary key? – dbc Nov 28 '17 at 22:54
  • Yes. The dictionary is `Dictionary`, where `Component` is a parent type that is inherited by many child types. Both the `Type` and `Component` of a single entry, are guaranteed to be of the same type. The `Assets.Logic.CompGroundType` in the example above is one such inherited type. This is part of an implementation of an [Entity Component System](https://en.wikipedia.org/wiki/Entity%E2%80%93component%E2%80%93system). I apologize for not being clearer earlier. If you need further explanation or code samples I would try to provide then. Thanks. – user92748 Nov 28 '17 at 23:10
  • An [mcve] would still be helpful. E.g. the presence of the `k__BackingField` property suggests that you set [`DefaultContractResolver.IgnoreSerializableAttribute = false`](https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_Serialization_DefaultContractResolver_IgnoreSerializableAttribute.htm), or maybe set [`MemberSerialization = MemberSerialization.Fields`](https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_MemberSerialization.htm) somewhere. We would need to know which you enabled, if either. – dbc Nov 28 '17 at 23:11
  • Just out of curiosity, do you have [`KeyedByTypeCollection`](https://msdn.microsoft.com/en-us/library/ms404549(v=vs.110).aspx) in Unity? It might work better here both for serialization and for API ease of use... If not, It seems as though, in [fastJSON Deserialization List](https://stackoverflow.com/q/34707871/3744182), the OP was able to successfully port `KeyedByTypeCollection` to Unity as suggested in the answer there. – dbc Nov 28 '17 at 23:22
  • I added a more complete example. I'll look into `KeyedByTypeCollection`, I wasn't aware of that. – user92748 Nov 28 '17 at 23:36
  • Unfortunately I dont seem to have `KeyedByTypeCollection` in Unity, but even if I did, I'm not sure how it would help with my issue. It would still be serialized similarly to the dictionary above, from what I understand, meaning the serialization of the keys would still be just as lengthy. I added a sample of the `CompGroundType` implementation in edit 4. – user92748 Nov 28 '17 at 23:58
  • Perhaps when you serialize your classes just dont include .NET nametypes. Check `TypeNameHandling.Auto` option of the `JsonConvert` class, please. – NoWar Feb 20 '19 at 06:25

1 Answers1

3

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.

  • This looks like exactly what I need. I implemented it, but unfortunately I'm writing this for Unity and it turns out the Json.NET unity fork has [a bug where setting a custom Binder is useless](https://github.com/SaladLab/Json.Net.Unity3D/issues/22), since it's functions are never called... – user92748 Nov 28 '17 at 19:20
  • Ok, I managed to fix that issue and use the full Json.NET library, not the Unity fork, but even after implementing the above example, 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, "k__BackingField": 1 } }` So like you can see, the "$type" field does only contain the name, but the dictionary key still contains the assembly. – user92748 Nov 28 '17 at 21:29
  • The assembly name in the dictionary key is coming from the serialization of the Type objects. I've updated the answer to cover custom serialization of objects based on type too. –  Nov 29 '17 at 08:55
  • I just reviewed the extra info I added and it was wrong, sorry about that. I've edited my answer to reflect that. –  Nov 29 '17 at 17:34
  • (I only now saw your last edit) Turns out dictionary keys are not serialized using a JsonConverter. I had to write a custom JsonConverter for the entire dictionary as explained in [this answer](https://stackoverflow.com/a/7010231/7038967). This also meant that I didn't even need the custom binder in the end. Anyway, thanks for pointing me to what eventually lead me in the correct direction. – user92748 Nov 29 '17 at 22:13