2012-06-19 12 views
28

Supongamos que tengo la siguiente jerarquía de clases:El uso de un discriminador de tipo personalizado para contar JSON.net qué tipo de una jerarquía de clases para deserializar

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

¿Es posible tener json.net uso de la propiedad (por ejemplo "tipo" o "discriminador") para determinar qué tipo el objeto cuando deserializa la organización? Por ejemplo, lo siguiente debería deserializar una instancia de Empresa.

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

Y lo siguiente debe deserializar una instancia de NonProfitOrganization.

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

Cuando llamo el siguiente:

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

donde la carga útil es los fragmentos anteriores JSON. Eché un vistazo a la configuración de "TypeNameHandling" en propiedades o clases pero serializa todo el tipo de .NET, que no es "portátil" entre el cliente y el servidor cuando las clases están definidas en diferentes espacios de nombres y ensamblajes.

Prefiero definir que el tipo es una manera neutral que los clientes escritos en cualquier idioma pueden usar para determinar el tipo real del tipo de objeto que se está serializando.

Respuesta

19

En caso de que todavía está buscando, aquí hay un ejemplo: http://james.newtonking.com/archive/2011/11/19/json-net-4-0-release-4-bug-fixes.aspx

Esto le permitirá crear una cartografía en la tabla:

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

El código tiene un ligero defecto de que si un tipo El mapeo de nombre se intenta cuando el tipo es único pero el nombre ya se utiliza, el método de Map emitirá una excepción después de que la asignación de tipo a nombre ya se haya agregado dejando la tabla en un estado incoherente.

+4

No sé cuál era la situación en 2012, pero creo ahora es mejor heredar de DefaultSerializationBinder y recurrir a implementaciones de base, porque entonces puede beneficiarse de su implementación precisa. –

8

Para llevar la respuesta de eulerfx aún más; Quería aplicar el atributo DisplayName a una clase y hacer que se convirtiera automáticamente en el nombre de tipo utilizado; con tal fin:

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

y ejemplo de uso:

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


    } 
} 

de salida:

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

He escrito solución puramente declarativa con la posibilidad de especificar campo discriminador de encargo, y proporcionar el nombre de ámbito manejo por clase base (a diferencia de usecure global JsonSerializationSettings, especialmente en diferentes Web-Api cuando no tenemos la capacidad de especificar JsonSerializationSettings personalizados).

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



} 

de salida:

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

se puede agarrar aquí:

https://dotnetfiddle.net/ELcvnk

+0

¡Me gusta por una gran contribución! ¿Hay alguna forma de que el parámetro "@type" aparezca primero en el json? –

+1

@ HenrikHolmgaardHøyer en su clase base, agregue una 'Orden' a su propiedad Json. '[JsonTag, JsonProperty (" @ type ", Order = Int32.MinValue)]' 'public string Type {get; conjunto; } ' –

2

Con otra aplicación JsonSubtypes convertidor.

Uso:

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

la implementación del convertidor se puede descargar directamente desde el repositorio: JsonSubtypes.cs y también es liviano como nuget package

Cuestiones relacionadas