2016-05-06 5 views
6

Ich habe einen JSON-Eingang ähnlich diesem vereinfachten Beispiel.Ist es möglich, in ein ExpandoObject (oder Dictionary) mit stark typisierten Membern zu deserialisieren, wo es möglich ist?

{ 
    "model1": { 
    "$type": "MyType, MyAssembly", 
    "A": 5 
    }, 
    "model2": { 
    "C": "something" 
} 

Was möchte ich erreichen möchte, ist ein „Hybrid“ Ergebnis, das ich meine, eine Top-Level ExpandoObject mit zwei Eigenschaften model1 und model2, ABER model1 eine starke Art von MyType haben würde (basierend auf der Json.NET type information. Da model2 keine Typinformationen hat, wäre es ein verschachteltes ExpandoObject. Diese Logik sollte auch über tiefere Verschachtelungsebenen hinweg gleich sein (siehe meine Aktualisierung), das Beispiel ist in dieser Hinsicht vereinfacht.

Mein Problem ist, dass ich die "Hybridität" nicht erreichen kann. Eine Art, wie ich ein komplett typisiertes Ergebnis bekommen könnte (Wenn das Objekt der obersten Ebene stark typisiert wäre), kann ich andererseits ein vollständig dynamisches Ergebnis haben (alles ist ExpandoObject oder die dritte Art, wie ich eine JObject haben könnte, die in diesem Szenario bedeutungslos ist.

// this will give a fully dynamic result, regardless the child type information 
var result = JsonConvert.DeserializeObject<ExpandoObject>(input, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto }); 

UPDATE

Ich habe gerade mit Deserialisierung in ein generisches IDictionary experimentiert, und auf diese Weise ich stark typisierte Ergebnisse für Top-Level-Kind-Eigenschaften bekommen, die mein Beispiel technisch löst. Auf niedrigeren Ebenen funktioniert es jedoch immer noch nicht und gibt ein JObject Ergebnis für nicht typisierte untergeordnete Eigenschaften. Insgesamt ist es also keine gute Lösung für meinen echten Anwendungsfall.

Antwort

3

Das Problem ist, dass Json.NET die ExpandoObjectConverter nicht einfach keine eigenen Metadaten-Eigenschaften handhaben wie "$type", "id" oder "$ref".

Da jedoch Json.NET ist Open Source und seine MIT-Lizenz allows modification kann die einfachste Lösung, um Ihre eigene Kopie von ExpandoObjectConverter zu machen und passen sie an Ihre Bedürfnisse, nach dem Vorbild der Json.NET Deserialization into dynamic object with referencing. Sie müssen auch einige Low-Level-JSON-Dienstprogramme kopieren:

var settings = new JsonSerializerSettings 
{ 
    Formatting = Newtonsoft.Json.Formatting.Indented, 
    TypeNameHandling = TypeNameHandling.Auto, 
    Converters = new [] { new TypeNameHandlingExpandoObjectConverter() }, 
}; 

var expando2 = JsonConvert.DeserializeObject<ExpandoObject>(input, settings); 

Prototype fiddle:

/// <summary> 
/// Converts an ExpandoObject to and from JSON. 
/// Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Converters/ExpandoObjectConverter.cs 
/// License: https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md 
/// </summary> 
public class TypeNameHandlingExpandoObjectConverter : JsonConverter 
{ 
    /// <summary> 
    /// Writes the JSON representation of the object. 
    /// </summary> 
    /// <param name="writer">The <see cref="JsonWriter"/> to write to.</param> 
    /// <param name="value">The value.</param> 
    /// <param name="serializer">The calling serializer.</param> 
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 
    { 
     // can write is set to false 
    } 

    /// <summary> 
    /// Reads the JSON representation of the object. 
    /// </summary> 
    /// <param name="reader">The <see cref="JsonReader"/> to read from.</param> 
    /// <param name="objectType">Type of the object.</param> 
    /// <param name="existingValue">The existing value of object being read.</param> 
    /// <param name="serializer">The calling serializer.</param> 
    /// <returns>The object value.</returns> 
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 
    { 
     return ReadValue(reader, serializer); 
    } 

    private object ReadValue(JsonReader reader, JsonSerializer serializer) 
    { 
     if (!reader.MoveToContent()) 
     { 
      throw JsonSerializationExceptionHelper.Create(reader, "Unexpected end when reading ExpandoObject."); 
     } 

     switch (reader.TokenType) 
     { 
      case JsonToken.StartObject: 
       return ReadObject(reader, serializer); 
      case JsonToken.StartArray: 
       return ReadList(reader, serializer); 
      default: 
       if (JsonTokenUtils.IsPrimitiveToken(reader.TokenType)) 
       { 
        return reader.Value; 
       } 

       throw JsonSerializationExceptionHelper.Create(reader, string.Format("Unexpected token when converting ExpandoObject: {0}", reader.TokenType)); 
     } 
    } 

    private object ReadList(JsonReader reader, JsonSerializer serializer) 
    { 
     IList<object> list = new List<object>(); 

     while (reader.Read()) 
     { 
      switch (reader.TokenType) 
      { 
       case JsonToken.Comment: 
        break; 
       default: 
        object v = ReadValue(reader, serializer); 

        list.Add(v); 
        break; 
       case JsonToken.EndArray: 
        return list; 
      } 
     } 

     throw JsonSerializationExceptionHelper.Create(reader, "Unexpected end when reading ExpandoObject."); 
    } 

    private object ReadObject(JsonReader reader, JsonSerializer serializer) 
    { 
     if (serializer.TypeNameHandling != TypeNameHandling.None) 
     { 
      var obj = JObject.Load(reader); 

      Type polymorphicType = null; 
      var polymorphicTypeString = (string)obj["$type"]; 
      if (polymorphicTypeString != null) 
      { 
       if (serializer.TypeNameHandling != TypeNameHandling.None) 
       { 
        string typeName, assemblyName; 
        ReflectionUtils.SplitFullyQualifiedTypeName(polymorphicTypeString, out typeName, out assemblyName); 
        polymorphicType = serializer.Binder.BindToType(assemblyName, typeName); 
       } 
       obj.Remove("$type"); 
      } 

      if (polymorphicType == null || polymorphicType == typeof(ExpandoObject)) 
      { 
       using (var subReader = obj.CreateReader()) 
        return ReadExpandoObject(subReader, serializer); 
      } 
      else 
      { 
       using (var subReader = obj.CreateReader()) 
        return serializer.Deserialize(subReader, polymorphicType); 
      } 
     } 
     else 
     { 
      return ReadExpandoObject(reader, serializer); 
     } 
    } 

    private object ReadExpandoObject(JsonReader reader, JsonSerializer serializer) 
    { 
     IDictionary<string, object> expandoObject = new ExpandoObject(); 

     while (reader.Read()) 
     { 
      switch (reader.TokenType) 
      { 
       case JsonToken.PropertyName: 
        string propertyName = reader.Value.ToString(); 

        if (!reader.Read()) 
        { 
         throw JsonSerializationExceptionHelper.Create(reader, "Unexpected end when reading ExpandoObject."); 
        } 

        object v = ReadValue(reader, serializer); 

        expandoObject[propertyName] = v; 
        break; 
       case JsonToken.Comment: 
        break; 
       case JsonToken.EndObject: 
        return expandoObject; 
      } 
     } 

     throw JsonSerializationExceptionHelper.Create(reader, "Unexpected end when reading ExpandoObject."); 
    } 

    /// <summary> 
    /// Determines whether this instance can convert the specified object type. 
    /// </summary> 
    /// <param name="objectType">Type of the object.</param> 
    /// <returns> 
    ///  <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>. 
    /// </returns> 
    public override bool CanConvert(Type objectType) 
    { 
     return (objectType == typeof(ExpandoObject)); 
    } 

    /// <summary> 
    /// Gets a value indicating whether this <see cref="JsonConverter"/> can write JSON. 
    /// </summary> 
    /// <value> 
    ///  <c>true</c> if this <see cref="JsonConverter"/> can write JSON; otherwise, <c>false</c>. 
    /// </value> 
    public override bool CanWrite 
    { 
     get { return false; } 
    } 
} 

internal static class JsonTokenUtils 
{ 
    // Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Utilities/JsonTokenUtils.cs 
    public static bool IsPrimitiveToken(this JsonToken token) 
    { 
     switch (token) 
     { 
      case JsonToken.Integer: 
      case JsonToken.Float: 
      case JsonToken.String: 
      case JsonToken.Boolean: 
      case JsonToken.Undefined: 
      case JsonToken.Null: 
      case JsonToken.Date: 
      case JsonToken.Bytes: 
       return true; 
      default: 
       return false; 
     } 
    } 
} 

internal static class JsonReaderExtensions 
{ 
    // Adapted from internal bool JsonReader.MoveToContent() 
    // https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/JsonReader.cs#L1145 
    public static bool MoveToContent(this JsonReader reader) 
    { 
     if (reader == null) 
      throw new ArgumentNullException(); 
     JsonToken t = reader.TokenType; 
     while (t == JsonToken.None || t == JsonToken.Comment) 
     { 
      if (!reader.Read()) 
      { 
       return false; 
      } 

      t = reader.TokenType; 
     } 

     return true; 
    } 
} 

internal static class JsonSerializationExceptionHelper 
{ 
    public static JsonSerializationException Create(this JsonReader reader, string format, params object[] args) 
    { 
     // Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/JsonPosition.cs 

     var lineInfo = reader as IJsonLineInfo; 
     var path = (reader == null ? null : reader.Path); 
     var message = string.Format(CultureInfo.InvariantCulture, format, args); 
     if (!message.EndsWith(Environment.NewLine, StringComparison.Ordinal)) 
     { 
      message = message.Trim(); 
      if (!message.EndsWith(".", StringComparison.Ordinal)) 
       message += "."; 
      message += " "; 
     } 
     message += string.Format(CultureInfo.InvariantCulture, "Path '{0}'", path); 
     if (lineInfo != null && lineInfo.HasLineInfo()) 
      message += string.Format(CultureInfo.InvariantCulture, ", line {0}, position {1}", lineInfo.LineNumber, lineInfo.LinePosition); 
     message += "."; 

     return new JsonSerializationException(message); 
    } 
} 

internal static class ReflectionUtils 
{ 
    // Utilities taken from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Utilities/ReflectionUtils.cs 
    // I couldn't find a way to access these directly. 

    public static void SplitFullyQualifiedTypeName(string fullyQualifiedTypeName, out string typeName, out string assemblyName) 
    { 
     int? assemblyDelimiterIndex = GetAssemblyDelimiterIndex(fullyQualifiedTypeName); 

     if (assemblyDelimiterIndex != null) 
     { 
      typeName = fullyQualifiedTypeName.Substring(0, assemblyDelimiterIndex.GetValueOrDefault()).Trim(); 
      assemblyName = fullyQualifiedTypeName.Substring(assemblyDelimiterIndex.GetValueOrDefault() + 1, fullyQualifiedTypeName.Length - assemblyDelimiterIndex.GetValueOrDefault() - 1).Trim(); 
     } 
     else 
     { 
      typeName = fullyQualifiedTypeName; 
      assemblyName = null; 
     } 
    } 

    private static int? GetAssemblyDelimiterIndex(string fullyQualifiedTypeName) 
    { 
     int scope = 0; 
     for (int i = 0; i < fullyQualifiedTypeName.Length; i++) 
     { 
      char current = fullyQualifiedTypeName[i]; 
      switch (current) 
      { 
       case '[': 
        scope++; 
        break; 
       case ']': 
        scope--; 
        break; 
       case ',': 
        if (scope == 0) 
        { 
         return i; 
        } 
        break; 
      } 
     } 

     return null; 
    } 
} 

es dann wie verwenden.

+0

Vielen Dank für die ausführliche Antwort, ich habe vorher über eine solche Lösung nachgedacht. Es funktioniert gut, aber ich habe es geschafft, mein Problem außerhalb von Json.NET-Kontext zu lösen (Ich brauchte dieses "Feature" für ASP.NET MVC-Modellbindung, und ich habe es geschafft, es allgemein in einem benutzerdefinierten Modellbinder zu implementieren). –

0

Hier ist, wie ich es tun würde:

void Main() 
{ 
    var json = "{\r\n \"model1\": {\r\n \"$type\": \"MyType, MyAssembly\",\r\n \"A\": 5\r\n },\r\n \"model2" + 
     "\": {\r\n \"C\": \"something\"\r\n}}"; 
    var result = JsonConvert.DeserializeObject<Result>(json); 
} 

public class Result 
{ 
    public MyType Model1 { get; set; } 
    public ExpandoObject Model2 { get; set;} 
} 
public class MyType { public int A { get; set;} } 

Sie können auch Result.Model2 geben eine Art dynamic (die Sie seine Eigenschaften mit Hilfe Syntax wie result.Model2.something zugreifen können), oder JSON.NET des JObject, das ist mehr JSON-orientiert.

jedoch, wenn Sie sagen, dass Sie nicht eine Klasse wie Result wollen, aber Sie wollen, dass die JSON des $type der Lage sein, eine bestimmte Instanz Typ zu bestimmen, können Sie die TypeNameHandling setting verwenden.

var result = JsonConvert.DeserializeObject<ExpandoObject>(
    json, 
    new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.Auto}); 

bewusst sein, nur, dass es Auswirkungen auf die Sicherheit, wenn Sie erlauben Client bereitgestellte JSON-Werte beliebiger Typen in Ihrem .NET-Umgebung zu instanziiert.

Verwandte Themen