2012-06-19 8 views
28

Załóżmy, że mam następujące klasy Hierarchia:Korzystanie z niestandardowego typu dyskryminator powiedzieć Json.NET jaki rodzaj hierarchii klas deserializacji

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 */ 
} 

to możliwe, aby mieć json.net użytkowania mienia („typ” słownie lub "discriminator"), aby określić, który typ obiektu, gdy deserializuje organizację? Na przykład poniższe elementy powinny deserializować instancję firmy.

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

I dodaje powinny deserializowania wystąpienie NonProfitOrganization.

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

Kiedy zadzwonić pod następujące:

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

gdzie ładunek jest wyżej fragmentów JSON. Spojrzałem na ustawienie "TypeNameHandling" na właściwościach lub klasach, ale serializuje on cały typ .NET, który nie jest "przenośny" między klientem a serwerem, gdy klasy są zdefiniowane w różnych przestrzeniach nazw i złożeniach.

Wolałbym zdefiniować typ, który jest neutralny sposób, który klienci napisani w dowolnym języku mogą użyć do określenia rzeczywistego typu serializowanego typu obiektu.

Odpowiedz

19

W przypadku, gdy wciąż szuka, oto przykład: http://james.newtonking.com/archive/2011/11/19/json-net-4-0-release-4-bug-fixes.aspx

To pozwoli na utworzenie mapowania opartą tabeli:

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); 
    } 
} 

Kod ma niewielką wadę, że jeśli typ Mapowanie nazw jest podejmowane w przypadku, gdy typ jest unikalny, ale nazwa jest już używana, metoda Map wygeneruje wyjątek po dodaniu mapowania typu nazwa do nazwy, pozostawiając tabelę w niespójnym stanie.

+4

nie wiem, jaka jest sytuacja była w 2012 roku, ale myślę, teraz lepiej jest dziedziczyć po DefaultSerializationBinder i wrócić do podstawowych implementacji, ponieważ wtedy możesz skorzystać z jego precyzyjnej implementacji. –

8

Aby dalej przyjmować odpowiedź Eulerfx; Chciałem zastosować atrybut DisplayName do klasy i automatycznie stać się używaną nazwą typu; w tym celu:

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); 
    } 
} 

i wykorzystanie przykład:

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)); 


    } 
} 

wyjściowa:

[ 
    { 
    "$type": "bool", 
    "name": "alive" 
    }, 
    { 
    "$type": "string", 
    "maxLength": 10, 
    "name": "name" 
    }, 
    { 
    "$type": "number", 
    "max": 120.0, 
    "name": "age" 
    }, 
    { 
    "$type": "enum", 
    "values": [ 
     "Single", 
     "Married" 
    ], 
    "name": "status" 
    } 
] 
5

Pisałem rozwiązanie czysto deklaratywny z możliwością zdefiniowania pola dyskryminacyjne, a świadczenia scoped nazwę obsługa na klasę podstawową (w przeciwieństwie do globalnych JsonSerializationSettings usecure, szczególnie w różnych Web-Api, gdy nie mamy możliwości określenia niestandardowych JsonSerializationSettings).

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); 
    } 



} 

wyjściowa:

{"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 

Można go pobrać tutaj:

https://dotnetfiddle.net/ELcvnk

+0

Kciuki za wielki wkład! Czy mimo to można uzyskać parametr "@type" jako pierwszy w jsonie? –

+1

@ HenrikHolmgaardHøyer w twojej klasie bazowej, dodaj 'Zamówienie' do twojej JsonProperty. '[JsonTag, JsonProperty (" @ type ", Order = Int32.MinValue)]' 'public string Type {get; zestaw; } ' –

2

Z innej realizacji JsonSubtypes kalkulator.

Zastosowanie:

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

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

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

    [TestMethod] 
    public void Demo() 
    { 
     var annimal = JsonConvert.DeserializeObject<Annimal>("{\"Sound\":\"Bark\",\"Breed\":\"Jack Russell Terrier\"}"); 
     Assert.AreEqual("Jack Russell Terrier", (annimal as Dog)?.Breed); 
    } 

realizacja konwerter można bezpośrednio pobrać z repozytorium: JsonSubtypes.cs i również one dostępne jako nuget package

Powiązane problemy