NewtonSoft SerializeObject 忽略 TypeNameHandling

yes*_*man 1 c# json.net

按照官方文档

string jsonTypeNameAll = JsonConvert.SerializeObject(stockholder, Formatting.Indented, new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All
});

Console.WriteLine(jsonTypeNameAll);
// {
//   "$type": "Newtonsoft.Json.Samples.Stockholder, Newtonsoft.Json.Tests",
//   "FullName": "Steve Stockholder",
//   "Businesses": {
//     "$type": "System.Collections.Generic.List`1[[Newtonsoft.Json.Samples.Business, Newtonsoft.Json.Tests]], mscorlib",
//     "$values": [
//       {
//         "$type": "Newtonsoft.Json.Samples.Hotel, Newtonsoft.Json.Tests",
//         "Stars": 4,
//         "Name": "Hudson Hotel"
//       }
//     ]
//   }
// }
Run Code Online (Sandbox Code Playgroud)

我已经复制粘贴了这段代码。

    public static string Convert<T>(T cacheObject)
    {
        return JsonConvert.SerializeObject(cacheObject, Formatting.Indented, new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.All
        });
    }
Run Code Online (Sandbox Code Playgroud)

但是,如果我用 调用它Convert(DateTime.Now),我会得到一个序列化的 DateTime 字符串,不包含类型:

在此输入图像描述

我究竟做错了什么?

dbc*_*dbc 5

TypeNameHandling"$type"其工作原理是向 JSON对象添加一个特殊的保留属性,指定序列化的 .Net 类型。此外,正如 Newtonsoft 的序列化指南中所解释的,数组将嵌套在指定类型的包装对象内:

请注意,如果在序列化器上为 JSON 数组启用了 TypeNameHandling 或 PreserveReferencesHandling,则 JSON 数组将包装在包含对象中。该对象将具有类型名称/引用属性和 $values 属性,该属性将包含集合数据。

然而,序列化 JSON 原语时并没有实现这种特殊情况。序列化为原语的 .Net 对象将作为原语发出,而不添加任何包装器,即使TypeNameHandling指定了也是如此。

因此,如果您想为 JSON 原语指定 .Net 类型信息,则必须自己创建一个包装器对象。一些示例可以在Deserialize Dictionary<string, object> with enum value in C# 的答案JSON.net (de)serialize untyped property答案中找到。

根据这些答案,创建以下包装器:

public abstract class TypeWrapper
{
    // Taken from this answer /sf/answers/2683826281/
    // To /sf/ask/2683547331/
    // By /sf/users/262092771/
    protected TypeWrapper() { }

    [JsonIgnore]
    public abstract object ObjectValue { get; }

    public static TypeWrapper CreateWrapper<T>(T value)
    {
        if (value == null)
            return new TypeWrapper<T>();
        var type = value.GetType();
        if (type == typeof(T))
            return new TypeWrapper<T>(value);
        // Return actual type of subclass
        return (TypeWrapper)Activator.CreateInstance(typeof(TypeWrapper<>).MakeGenericType(type), value);
    }
}

public sealed class TypeWrapper<T> : TypeWrapper
{
    public TypeWrapper() : base() { }

    public TypeWrapper(T value)
        : base()
    {
        this.Value = value;
    }

    public override object ObjectValue { get { return Value; } }

    public T Value { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

然后修改你的Convert<T>()如下:

public static partial class JsonExtensions
{
    static readonly IContractResolver globalResolver = new JsonSerializer().ContractResolver;
    
    public static string Convert<T>(T cacheObject)
    {
        return JsonConvert.SerializeObject(ToTypeWrapperIfRequired(cacheObject), Formatting.Indented, new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.All
        });
    }
    
    public static T UnConvert<T>(string json)
    {
        var obj = JsonConvert.DeserializeObject<object>(json, new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.All
        });
        if ((obj is TypeWrapper wrapper))
            return (T)wrapper.ObjectValue;
        return (T)obj;
    }
    
    static object ToTypeWrapperIfRequired<T>(T obj, IContractResolver resolver = null)
    {
        resolver = resolver ?? globalResolver;
        if (obj == null)
            return null;
        // Type information is redundant for string or bool
        if (obj is bool || obj is string)
            return obj;
        var contract = resolver.ResolveContract(obj.GetType());
        if (contract is JsonPrimitiveContract)
            return TypeWrapper.CreateWrapper(obj);
        return obj;
    }
}
Run Code Online (Sandbox Code Playgroud)

现在您可以转换为 JSON 并返回,如下所示:

var json = JsonExtensions.Convert(obj);

var objBack = JsonExtensions.UnConvert<T>(json);
Run Code Online (Sandbox Code Playgroud)

笔记:

  • 仅在必要时才会将包装器添加到正在序列化的对象中 - 即,如果 .Net 对象将被序列化为 JSON 原语而不是 JSON 对象或数组。此外,如果传入对象是 astring或 a,我不会添加包装器bool,因为这些类型可以从 JSON 中明确推断出来。

  • 去做:

    • 决定是否需要保留空值可为空结构的类型。如果是这样,他们也需要包装。
    • 决定往返JValue.
    • Convert<T>()序列化和反序列化已包装的对象会导致其在反序列化时解开包装,因此如果传入对象是包装器,您可能需要抛出异常。
  • 请注意,使用TypeNameHandling可能会给您的应用程序带来安全漏洞。有关详细信息,请参阅Newtonsoft Json 中的 TypeNameHandling 警告由于 Json.Net TypeNameHandling auto 而导致外部 json 易受攻击?

  • 另请注意,类型名称信息并不总是可以在不同的 .Net 框架之间移植。例如,请参见不同框架中的序列化和反序列化#1378。在这种情况下您可能需要编写自定义ISerializationBinder

演示小提琴在这里