2012-06-19 11 views
28

Supponiamo che io ho la seguente gerarchia di classe:Utilizzando un tipo personalizzato discriminatore per dire JSON.net quale tipo di una gerarchia di classi per deserializzare

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

E 'possibile avere json.net alcune stanze sono (diciamo "tipo" o "discriminatore") per determinare quale tipo l'oggetto deve deserializzare l'organizzazione? Ad esempio, il seguente dovrebbe deserializzare un'istanza di Company.

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

E quanto segue deve deserializzare un'istanza di NonProfitOrganization.

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

Quando chiamo il seguente:

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

cui carico utile è i frammenti JSON di cui sopra. Ho dato un'occhiata all'impostazione di "TypeNameHandling" su proprietà o classi ma serializza l'intero tipo .NET, che non è "portatile" tra client e server quando le classi sono definite in spazi dei nomi e assiemi diversi.

Preferisco definire il tipo in modo neutro che i client scritti in qualsiasi linguaggio possono utilizzare per determinare il tipo effettivo del tipo di oggetto che viene serializzato.

risposta

19

Nel caso in cui si sta ancora cercando, ecco un esempio: http://james.newtonking.com/archive/2011/11/19/json-net-4-0-release-4-bug-fixes.aspx

Questo vi permetterà di creare una mappatura basata tavolo:

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

Il codice ha un lieve difetto di che, se un tipo di si tenta di mappare il nome laddove il tipo è univoco ma il nome è già utilizzato, il metodo Map genererà un'eccezione dopo che il mapping di tipo nome è già stato aggiunto lasciando la tabella in uno stato incoerente.

+4

Non so quale sia la situazione era in 2012, ma credo ora è meglio ereditare da DefaultSerializationBinder e ricorrere alle implementazioni di base, perché in questo modo è possibile trarre vantaggio dalla sua implementazione precisa. –

8

Per prendere ulteriormente la risposta di eulerfx; Volevo applicare l'attributo DisplayName a una classe e farlo diventare automaticamente il nome del tipo utilizzato; a tal fine:

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

e l'uso di esempio:

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


    } 
} 

uscita:

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

ho scritto soluzione puramente dichiarativa con possibilità di specificare campo discriminatore personalizzato, e fornire il nome ambito gestione per classe base (al contrario di usicure globali JsonSerializationSettings, specialmente su diversi Web-Api quando non abbiamo la possibilità di specificare JsonSerializationSettings personalizzate).

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



} 

uscita:

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

si può afferrare qui:

https://dotnetfiddle.net/ELcvnk

+0

Complimenti per un grande contributo! Esiste comunque il parametro "@type" per comparire per primo nel json? –

+1

@ HenrikHolmgaardHøyer nella classe base, aggiungi un 'Ordine' a JsonProperty. '[JsonTag, JsonProperty (" @ type ", Order = Int32.MinValue)]' 'public string Tipo {get; impostato; } ' –

2

Con un'altra implementazione JsonSubtypes convertitore.

Usage:

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

l'attuazione convertitore può essere scaricato direttamente dal repository: JsonSubtypes.cs ed è anche availble come nuget package

Problemi correlati