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;
    }
}
9
  • One option would be to use DefaultAssemblyBinder from Json serialization for Object Data Type. Another would be SimpleAssemblyMappingSerializationBinder from JsonConverter how to deserialize to generic object. Commented Nov 28, 2017 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. Commented Nov 28, 2017 at 21:40
  • I think we may need to see a minimal reproducible example 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<Type, T> where T is guaranteed to be of the same type as the dictionary key? Commented Nov 28, 2017 at 22:54
  • Yes. The dictionary is Dictionary<Type, Component>, 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. I apologize for not being clearer earlier. If you need further explanation or code samples I would try to provide then. Thanks. Commented Nov 28, 2017 at 23:10
  • An minimal reproducible example would still be helpful. E.g. the presence of the k__BackingField property suggests that you set DefaultContractResolver.IgnoreSerializableAttribute = false, or maybe set MemberSerialization = MemberSerialization.Fields somewhere. We would need to know which you enabled, if either. Commented Nov 28, 2017 at 23:11

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

5 Comments

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, since it's functions are never called...
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, "<GroundType>k__BackingField": 1 } } So like you can see, the "$type" field does only contain the name, but the dictionary key still contains the assembly.
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.
I just reviewed the extra info I added and it was wrong, sorry about that. I've edited my answer to reflect that.
(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. 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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.