JamesNK / Newtonsoft.Json

Json.NET is a popular high-performance JSON framework for .NET
https://www.newtonsoft.com/json
MIT License
10.83k stars 3.26k forks source link

Cannot control serialization based on declared type #1940

Open floyd-may opened 5 years ago

floyd-may commented 5 years ago

I'd like to be able to serialize according to the declared property type rather than the concrete type at runtime.

Source/destination types

class OuterClass
{
    public IMessage Message { get; set; }
}

interface IMessage
{
    string MessageProp { get; set; }
}

class Message : IMessage
{
    public string MessageProp { get; set; }

    public string OtherProp { get; set; }
}

Expected behavior

I would like to be able to easily configure the serializer to do this:

{
  "Message": {
    "MessageProp": "I should be visible"
  }
}

Actual behavior

This is how a message would get serialized by default given the above types.

{
  "Message": {
    "MessageProp": "I should be visible",
    "OtherProp": "I should not be visible"
  }
}

Workaround

class Resolver : DefaultContractResolver
{
    private readonly ConcurrentDictionary<Type, Type> _interfaceMapping;
    private readonly IEnumerable<Type> _preferredTypes;

    public Resolver(IEnumerable<Type> preferredTypes)
    {
        _preferredTypes = preferredTypes;
        _interfaceMapping = new ConcurrentDictionary<Type, Type>();
    }

    public override JsonContract ResolveContract(Type type)
    {
        var contractType = _interfaceMapping.GetOrAdd(type, FindPreferredType);

        return base.ResolveContract(contractType);
    }

    private Type FindPreferredType(Type concreteType)
    {
        var preferred = _preferredTypes
            .Where(x => x.IsAssignableFrom(concreteType))
            .SingleOrDefault();

        return preferred ?? concreteType;
    }

}

This workaround makes some pretty egregious assumptions. If you expect that the entirety of Message and not just IMessage to get serialized in any scenario, this will do The Wrong Thing (tm).

Proposal

I believe that the core of the issue is that the IContractResolver interface has no knowledge of what the declared property type is. I would humbly suggest adding an additional Type parameter containing the declared type so that the resolver can make the determination of what contract to resolve. Given the above types, I would love to be able to write a custom contract resolver that looks like this:

class Resolver : DefaultContractResolver
{
    public override JsonContract ResolveContract(Type type, Type declaredType)
    {
        return base.ResolveContract(declaredType, null);
    }
}
floyd-may commented 5 years ago

Some additional context:

I spent some time looking through the source code to see if a hook existed to do what I wanted. That search led me to here: https://github.com/JamesNK/Newtonsoft.Json/blob/d48558b53b0a516bfa15c1af50231ea1ba7c2454/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs#L518-L524

The memberContract variable gets set to the contract corresponding to the run-time type of the property's value unless the property type happens to be sealed. There doesn't appear to be a way to override this behavior hence the hackish workaround above.

masaeedu commented 4 years ago

I agree this would be useful. Without it, it's impossible to serialize things like COM objects accurately, all of which have an identical, opaque runtime representation. The only way to correctly serialize these values is in via type directed serialization.