44

假设我有以下类层次结构:

public abstract class Organization
{
    /* properties related to all organizations */
}

public sealed class Company : Organization
{
     /* properties related to companies */
} 

public sealed class NonProfitOrganization : Organization
{
     /* properties related to non profit organizations */
}

是否可以让json.net使用属性(比如“类型”或“鉴别器”)来确定对象在反序列化组织时的类型?例如,下面应该反序列化 Company 的一个实例。

{
   "type": "company"
   /* other properties related to companies */
}

下面应该反序列化 NonProfitOrganization 的一个实例。

{
   "type": "non-profit"
   /* other properties related to non profit */
}

当我调用以下内容时:

Organization organization = JsonConvert.DeserializeObject<Organization>(payload);

其中有效负载是上面的 JSON 片段。我查看了在属性或类上设置“ TypeNameHandling ”,但它序列化了整个 .NET 类型,当类在不同的命名空间和程序集中定义时,它在客户端和服务器之间不是“可移植的”。

我宁愿将类型定义为一种中性的方式,用任何语言编写的客户端都可以使用它来确定被序列化的对象类型的实际类型。

4

5 回答 5

22

如果你还在寻找,这里有一个例子:http: //james.newtonking.com/archive/2011/11/19/json-net-4-0-release-4-bug-fixes.aspx

这将允许您创建基于表的映射:

public class TypeNameSerializationBinder : SerializationBinder
{
    public TypeNameSerializationBinder(Dictionary<Type, string> typeNames = null)
    {
        if (typeNames != null)
        {
            foreach (var typeName in typeNames)
            {
                Map(typeName.Key, typeName.Value);
            }
        }
    }

    readonly Dictionary<Type, string> typeToName = new Dictionary<Type, string>();
    readonly Dictionary<string, Type> nameToType = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);

    public void Map(Type type, string name)
    {
        this.typeToName.Add(type, name);
        this.nameToType.Add(name, type);
    }

    public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        var name = typeToName.Get(serializedType);
        if (name != null)
        {
            assemblyName = null;
            typeName = name;
        }
        else
        {
            assemblyName = serializedType.Assembly.FullName;
            typeName = serializedType.FullName;                
        }
    }

    public override Type BindToType(string assemblyName, string typeName)
    {
        if (assemblyName == null)
        {
            var type = this.nameToType.Get(typeName);
            if (type != null)
            {
                return type;
            }
        }
        return Type.GetType(string.Format("{0}, {1}", typeName, assemblyName), true);
    }
}

代码有一个小缺陷,如果在类型唯一但名称已被使用的情况下尝试类型名称映射,则 Map 方法将在已添加类型到名称映射后抛出异常,将表留在不一致的状态。

于 2012-08-30T18:55:45.207 回答
12

进一步了解eulerfx的答案;我想将DisplayName属性应用到一个类并让它自动成为使用的类型名称;为此:

public class DisplayNameSerializationBinder : DefaultSerializationBinder
{
    private Dictionary<string, Type> _nameToType;
    private Dictionary<Type, string> _typeToName;

    public DisplayNameSerializationBinder()
    {
        var customDisplayNameTypes =
            this.GetType()
                .Assembly
                //concat with references if desired
                .GetTypes()
                .Where(x => x
                    .GetCustomAttributes(false)
                    .Any(y => y is DisplayNameAttribute));

        _nameToType = customDisplayNameTypes.ToDictionary(
            t => t.GetCustomAttributes(false).OfType<DisplayNameAttribute>().First().DisplayName,
            t => t);

        _typeToName = _nameToType.ToDictionary(
            t => t.Value,
            t => t.Key);

    }

    public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        if (false == _typeToName.ContainsKey(serializedType))
        {
            base.BindToName(serializedType, out assemblyName, out typeName);
            return;
        }

        var name = _typeToName[serializedType];

        assemblyName = null;
        typeName = name;
    }

    public override Type BindToType(string assemblyName, string typeName)
    {
        if (_nameToType.ContainsKey(typeName))
            return _nameToType[typeName];

        return base.BindToType(assemblyName, typeName);
    }
}

和用法示例:

public class Parameter
{
    public string Name { get; set; }
};

[DisplayName("bool")]
public class BooleanParameter : Parameter
{
}

[DisplayName("string")]
public class StringParameter : Parameter
{
    public int MinLength { get; set; }
    public int MaxLength { get; set; }
}

[DisplayName("number")]
public class NumberParameter : Parameter
{
    public double Min { get; set; }
    public double Max { get; set; }
    public string Unit { get; set; }
}

[DisplayName("enum")]
public class EnumParameter : Parameter
{
    public string[] Values { get; set; }
}

internal class Program
{
    private static void Main(string[] args)
    {
        var parameters = new Parameter[]
        {
            new BooleanParameter() {Name = "alive"},
            new StringParameter() {Name = "name", MinLength = 0, MaxLength = 10},
            new NumberParameter() {Name = "age", Min = 0, Max = 120},
            new EnumParameter() {Name = "status", Values = new[] {"Single", "Married"}}
        };

        JsonConvert.DefaultSettings = () => new JsonSerializerSettings
        {
            Binder = new DisplayNameSerializationBinder(),
            TypeNameHandling = TypeNameHandling.Auto,
            NullValueHandling = NullValueHandling.Ignore,
            DefaultValueHandling = DefaultValueHandling.Ignore,
            Formatting = Formatting.Indented,
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        };

        var json = JsonConvert.SerializeObject(parameters);
        var loadedParams = JsonConvert.DeserializeObject<Parameter[]>(json);
        Console.WriteLine(JsonConvert.SerializeObject(loadedParams));


    }
}

输出:

[
  {
    "$type": "bool",
    "name": "alive"
  },
  {
    "$type": "string",
    "maxLength": 10,
    "name": "name"
  },
  {
    "$type": "number",
    "max": 120.0,
    "name": "age"
  },
  {
    "$type": "enum",
    "values": [
      "Single",
      "Married"
    ],
    "name": "status"
  }
]
于 2015-11-26T15:17:17.743 回答
10

我编写了纯粹的声明性解决方案,能够指定自定义鉴别器字段,并为每个基类提供范围名称处理(与 usecure 全局 JsonSerializationSettings 相反,尤其是在我们无法指定自定义 JsonSerializationSettings 时在不同的 Web-Api 上)。

using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Reflection;
using System.Linq;
using System.Collections.Generic;

//  Discriminated Json Converter (JsonSubtypes) implementation for .NET
//
//  MIT License
//
//  Copyright (c) 2016 Anatoly Ressin

//  Permission is hereby granted, free of charge, to any person obtaining a 
//  copy of this software and associated documentation files (the "Software"), 
//  to deal in the Software without restriction, including without limitation 
//  the rights to use, copy, modify, merge, publish, distribute, sublicense, 
//  and/or sell copies of the Software, and to permit persons to whom the 
//  Software is furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in 
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
//  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
//  DEALINGS IN THE SOFTWARE.

////////////////////// USAGE ////////////////////////////////////////////////////////////////////////////////


[JsonConverter(typeof(JsonSubtypes))]     // Discriminated base class SHOULD NOT be abstract
public class ShapeBase {
    [JsonTag, JsonProperty("@type")]      // it SHOULD contain a property marked with [JsonTag]
    public string Type {get;set;}         // only one [JsonTag] annotation allowed per discriminated class

                                          // it COULD contain other properties, however this is NOT RECOMMENDED
                                          // Rationale: instances of this class will be created at deserialization
                                          // only for tag sniffing, and then thrown away.
}

public abstract class Shape: ShapeBase {  // If you want abstract parent - extend the root
    public abstract double GetArea();     // with needed abstract stuff, then use this class everywhere (see DEMO below)
}

[JsonSubtype("circle")]                   // Every final class-case SHOULD be marked with [JsonSubtype(tagValue)]
public class Circle: Shape {              // Two disctinct variant classes MUST have distinct tagValues

    [JsonProperty("super-radius")]        // You CAN use any Json-related annotation as well
    public double Radius { get; set; }     
    public override double GetArea() {
        return Radius * Radius * Math.PI;
    }
}                                         

[JsonSubtype("rectangle")]
public class Rectangle: Shape {
    public double Height { get; set; }
    public double Width { get; set; }
    public override double GetArea() {
        return Width * Height;
    }
}

[JsonSubtype("group")]
public class Group: Shape {
    [JsonProperty("shapes")]
    public List<Shape> Items { get; set; }
    public override double GetArea() {
        return Items.Select(item => item.GetArea()).Sum();
    }
}


                                          // Every final class-case SHOULD be registered with JsonSubtypes.register(typeof(YourConcreteClass))
                                          // either manually or with auto-register capability:
                                          // You can auto-register all classes marked with [JsonSubtype(tag)] in given Assembly
                                          // using JsonSubtypes.autoRegister(yourAssembly)



////////////////// DEMO /////////////////////////////////////////////////////////////////////////////////



public class Program
{
    public static void Main()
    {
        JsonSubtypes.autoRegister(Assembly.GetExecutingAssembly()); 
        Shape original = new Group() {
            Items = new List<Shape> {
                new Circle() { Radius = 5 }, 
                new Rectangle() { Height = 10, Width = 20 }
            }
        };
        string str = JsonConvert.SerializeObject(original);
        Console.WriteLine(str);
        var copy = JsonConvert.DeserializeObject(str,typeof(Shape)) as Shape; 

        // Note: we can deserialize object using any class from the hierarchy. 
        // Under the hood, anyway, it will be deserialized using the top-most 
        // base class annotated with [JsonConverter(typeof(JsonSubtypes))].
        // Thus, only soft-casts ("as"-style) are safe here.

        Console.WriteLine("original.area = {0}, copy.area = {1}", original.GetArea(), copy.GetArea());


    }
}



//////////////////////// IMPLEMENTATION //////////////////////////////////////////////////////////////////



public class JsonSubtypeClashException: Exception {

    public string TagValue { get; private set;}
    public Type RootType { get; private set; }
    public Type OldType { get; private set; }
    public Type NewType { get; private set; }

    public JsonSubtypeClashException(Type rootType, string tagValue, Type oldType, Type newType): base(
        String.Format(
            "JsonSubtype Clash for {0}[tag={1}]: oldType = {2}, newType = {3}",
            rootType.FullName,
            tagValue,
            oldType.FullName,
            newType.FullName
        )
    ) {
        TagValue = tagValue;
        RootType = rootType;
        OldType = oldType;
        NewType = newType;
    }
}

public class JsonSubtypeNoRootException: Exception {
    public Type SubType { get; private set; }

    public JsonSubtypeNoRootException(Type subType): base(
        String.Format(
            "{0} should be inherited from the class with the [JsonConverter(typeof(JsonSubtypes))] attribute",
            subType.FullName
        )
    ) {
        SubType = subType;
    }

}

public class JsonSubtypeNoTagException: Exception {
    public Type SubType { get; private set; }

    public JsonSubtypeNoTagException(Type subType): base(
        String.Format(
            @"{0} should have [JsonSubtype(""..."")] attribute",
            subType.FullName
        )
    ) {
        SubType = subType;
    }

}

public class JsonSubtypeNotRegisteredException: Exception {
    public Type Root { get; private set; }
    public string TagValue { get; private set; }
    public JsonSubtypeNotRegisteredException(Type root, string tagValue): base(
        String.Format(
            @"Unknown tag={1} for class {0}",
            root.FullName,
            tagValue
        )
    ) {
        Root = root;
        TagValue = tagValue;
    }
}


[AttributeUsage(AttributeTargets.Class)]
public class JsonSubtypeAttribute: Attribute {
    private string tagValue;
    public JsonSubtypeAttribute(string tagValue) {
        this.tagValue = tagValue;
    }
    public string TagValue {
        get {
            return tagValue;
        }
    }

}


public static class JsonSubtypesExtension {

    public static bool TryGetAttribute<T>(this Type t, out T attribute) where T: Attribute {
        attribute = t.GetCustomAttributes(typeof(T), false).Cast<T>().FirstOrDefault();
        return attribute != null;
    }

    private static Dictionary<Type, PropertyInfo> tagProperties = new Dictionary<Type, PropertyInfo>();

    public static bool TryGetTagProperty(this Type t, out PropertyInfo tagProperty) {
        if (!tagProperties.TryGetValue(t, out tagProperty)) {
            JsonConverterAttribute conv;
            if (t.TryGetAttribute(out conv) && conv.ConverterType == typeof(JsonSubtypes)) {
                var props = (from prop in t.GetProperties() where prop.GetCustomAttribute(typeof(JsonTagAttribute)) != null select prop).ToArray();
                if (props.Length == 0) throw new Exception("No tag");
                if (props.Length > 1) throw new Exception("Multiple tags");
                tagProperty = props[0];
            } else {
                tagProperty = null;
            }
            tagProperties[t] = tagProperty;

        }
        return tagProperty != null;
    }

    public static bool TryGetTagValue(this Type t, out string tagValue) {
        JsonSubtypeAttribute subtype;
        if (t.TryGetAttribute(out subtype)) {
            tagValue = subtype.TagValue;
            return true;
        } else {
            tagValue = null;
            return false;
        }
    }

    public static bool TryGetJsonRoot(this Type t, out Type root, out PropertyInfo tagProperty) {
        root = t;
        do {
            if (root.TryGetTagProperty(out tagProperty)) {
                return true;
            }
            root = root.BaseType;
        } while (t != null);
        return false;
    }
}


public class JsonTagAttribute: Attribute {
}

public class JsonTagInfo {
    public PropertyInfo Property { get; set; }
    public string Value { get; set; }
}

public class JsonRootInfo {
    public PropertyInfo Property { get; set; }
    public Type Root { get; set; }
}


public abstract class DefaultJsonConverter: JsonConverter {

    [ThreadStatic]
    private static bool silentWrite;

    [ThreadStatic]
    private static bool silentRead;

    public sealed override bool CanWrite {
        get {
            var canWrite = !silentWrite;
            silentWrite = false;
            return canWrite;
        }
    }

    public sealed override bool CanRead {
        get {
            var canRead = !silentRead;
            silentRead = false;
            return canRead;
        }
    }

    protected void _WriteJson(JsonWriter writer, Object value,  JsonSerializer serializer) {
        silentWrite = true;
        serializer.Serialize(writer, value);
    }

    protected Object _ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer) {       
        silentRead = true;
        return serializer.Deserialize(reader, objectType);
    }

}



public class JsonSubtypes: DefaultJsonConverter {

    private static Dictionary<Type, Dictionary<string, Type>> implementations = new Dictionary<Type, Dictionary<string, Type>>();
    private static Dictionary<Type, JsonTagInfo> tags = new Dictionary<Type, JsonTagInfo>();    
    private static Dictionary<Type, JsonRootInfo> roots = new Dictionary<Type, JsonRootInfo>();


    public static void register(Type newType) {
        PropertyInfo tagProperty;
        Type root;
        if (newType.TryGetJsonRoot(out root, out tagProperty)) {
            for(var t = newType; t != root; t = t.BaseType) {
                roots[t] = new JsonRootInfo() {
                    Property = tagProperty,
                    Root = root
                };
            }
            roots[root] = new JsonRootInfo() {
                    Property = tagProperty,
                    Root = root
            };
            Dictionary<string, Type> implementationMap;
            if (!implementations.TryGetValue(root, out implementationMap)) {
                implementationMap = new Dictionary<string, Type>();
                implementations[root] = implementationMap;
            }
            JsonSubtypeAttribute attr;
            if (!newType.TryGetAttribute(out attr)) {
                throw new JsonSubtypeNoTagException(newType);
            }
            var tagValue = attr.TagValue;
            Type oldType;
            if (implementationMap.TryGetValue(tagValue, out oldType)) {
                throw new JsonSubtypeClashException(root, tagValue, oldType, newType);
            } 
            implementationMap[tagValue] = newType;
            tags[newType] = new JsonTagInfo() {
                Property = tagProperty,
                Value = tagValue
            };

        } else {
            throw new JsonSubtypeNoRootException(newType);
        }
    }

    public static void autoRegister(Assembly assembly) {
        foreach(var type in assembly.GetTypes().Where(type => type.GetCustomAttribute<JsonSubtypeAttribute>() != null)) {
            register(type);
        }       
    }


    public override bool CanConvert(Type t) {
        return true;
    }

    public static T EnsureTag<T>(T value) {
        JsonTagInfo tagInfo;
        if (tags.TryGetValue(value.GetType(), out tagInfo)) {
            tagInfo.Property.SetValue(value, tagInfo.Value);
        }
        return value;
    }

    public override void WriteJson(JsonWriter writer, Object value, JsonSerializer serializer) {
        _WriteJson(writer, EnsureTag(value), serializer);
    }

    public override Object ReadJson(JsonReader reader, Type objectType, Object existingValue,   JsonSerializer serializer) {
        JsonTagInfo tagInfo;
        if (tags.TryGetValue(objectType, out tagInfo)) {
            return _ReadJson(reader, objectType, existingValue, serializer);
        } else {
            JsonRootInfo rootInfo;
            if (roots.TryGetValue(objectType, out rootInfo)) {
                JToken t = JToken.ReadFrom(reader);
                var stub = _ReadJson(t.CreateReader(), rootInfo.Root, existingValue, serializer);
                var tagValue = rootInfo.Property.GetValue(stub) as string;
                var implementationMap = implementations[rootInfo.Root];
                Type implementation;
                if (implementationMap.TryGetValue(tagValue, out implementation)) {
                    return ReadJson(t.CreateReader(), implementation, null, serializer);
                } else {
                    throw new JsonSubtypeNotRegisteredException(rootInfo.Root, tagValue);
                }
            } else {
                return _ReadJson(reader, objectType, existingValue, serializer);
            }
        }
    }

    public static T Deserialize<T>(string s) where T: class {
        return JsonConvert.DeserializeObject(s, typeof(T)) as T;
    }

    public static string Serialize<T>(T value) where T: class {
        return JsonConvert.SerializeObject(value);
    }



}

输出:

{"shapes":[{"super-radius":5.0,"@type":"circle"},{"Height":10.0,"Width":20.0,"@type":"rectangle"}],"@type":"group"}
original.area = 278.539816339745, copy.area = 278.539816339745

你可以在这里抓住它:

https://dotnetfiddle.net/ELcvnk

于 2016-03-22T06:31:48.103 回答
7

使用另一个JsonSubtypes转换器实现。

用法:

[JsonConverter(typeof(JsonSubtypes), "Sound")]
[JsonSubtypes.KnownSubType(typeof(Dog), "Bark")]
[JsonSubtypes.KnownSubType(typeof(Cat), "Meow")]
public class Animal
{
    public virtual string Sound { get; }
    public string Color { get; set; }
}

public class Dog : Animal
{
    public override string Sound { get; } = "Bark";
    public string Breed { get; set; }
}

public class Cat : Animal
{
    public override string Sound { get; } = "Meow";
    public bool Declawed { get; set; }
}

[TestMethod]
public void Demo()
{
    var input = @"{""Sound"":""Bark"",""Breed"":""Jack Russell Terrier""}"
    var animal = JsonConvert.DeserializeObject<Animal>(input);

    Assert.AreEqual("Jack Russell Terrier", (animal as Dog)?.Breed);
}

转换器实现可以直接从存储库下载:JsonSubtypes.cs也可以作为nuget 包使用

于 2017-06-19T22:08:19.683 回答
2

使用这个JsonKnownTypes,它的使用方式非常相似,添加几个属性:

[JsonConverter(typeof(JsonKnownTypeConverter<Organization>))]
[JsonDiscriminator(Name = "discriminator")]
[JsonKnownType(typeof(Company), "company")]
[JsonKnownType(typeof(NonProfitOrganization), "non-profit")]
public abstract class Organization
{
    /* properties related to all organizations */
}

public sealed class Company : Organization
{
     /* properties related to companies */
} 

public sealed class NonProfitOrganization : Organization
{
     /* properties related to non profit organizations */
}

并序列化:

var json = JsonConvert.SerializeObject(youObject)

输出json:

{..., "discriminator":"non-profit"} //if object was NonProfitOrganization

反序列化:

var organization = JsonConvert.DeserializeObject<Organization>(payload);
于 2020-02-19T09:41:29.087 回答