2012-09-20 9 views
13

Fondamentalmente, voglio includere o omettere una proprietà dal Json generato in base al suo valore al momento della serializzazione.Opzionalmente serializzare una proprietà in base al suo valore di runtime

Più specificamente, ho un tipo che sa se un valore è stato assegnato ad esso e ho solo voglia di serializzare le proprietà di quel tipo se ci ha stato qualcosa ad essa assegnati (quindi ho bisogno di ispezionare il valore in fase di esecuzione). Sto cercando di facilitare la mia API per rilevare la differenza tra "ha il valore predefinito" e "non è stato specificato affatto".

Un JsonConverter personalizzato non sembra sufficiente; L'ho provato e credo che il nome della proprietà sia già serializzato prima che il convertitore venga chiamato. Nel mio caso voglio omettere anche il nome della proprietà.

Ho cercato di estendere DefaultContractResolver ma CreateProperty e CreateProperties (che restituiscono i metadati di serializzazione JsonProperty) richiedono solo il tipo serializzato, quindi non riesco a ispezionare l'istanza stessa. In generale, non vedo nulla su DefaultContractResolver che mi consente di controllare se un'istanza è serializzata; forse l'ho perso

Ho anche pensato che forse avevo bisogno di creare un ContractResolver che restituisse un JSONObjectContract personalizzato per il mio tipo. Ma, ancora, non vedo nulla su JsonObjectContract che prende decisioni basate su un'istanza.

C'è un buon modo per raggiungere il mio obiettivo? Mi sto perdendo qualcosa di semplice? Ogni aiuto che puoi fornire è molto apprezzato. Poiché Json.NET è così estensibile, ho pensato che non sarebbe stato troppo difficile. Ma sto iniziando a pensare che io sia lontano dalle erbacce qui. :)

+0

Non sarebbe meglio assicurarsi che i dati che si stanno inserendo siano nel formato corretto per la serializzazione? Utilizzare in modo specifico un tipo o proiezione non standard in una query LINQ per ottenere i dati nel modo desiderato. –

+0

@RickStrahl - Mi capita di lavorare con Amber. Il motivo per cui questo non funzionerà è che i campi da includere o escludere sono dinamici e parte del protocollo che stiamo usando. In altre parole, un campo che non è incluso nella serializzazione significa qualcosa di diverso dal campo che appare con un certo valore. –

risposta

7

Ok, dopo aver scavato in giro fonte Json.NET per un po ', ho finalmente ottenuto questo lavoro e sarà anche onorare la ShouldSerialize * e * membri specificato che Json.NET supporta. State attenti: questo sta sicuramente andando nelle erbacce.

Così mi sono reso conto che la classe JsonProperty restituito da DefaultContractResolver.CreateProperty ha ShouldSerialize e convertitore di proprietà, che mi permettono di specificare se dovrebbe in realtà essere serializzato l'istanza di proprietà e, in caso affermativo, come farlo.

La deserializzazione richiede qualcosa di leggermente diverso. DefaultContractResolver.ResolveContract, per impostazione predefinita per un tipo personalizzato, restituisce un oggetto JsonObjectContract con una proprietà Convertitore nullo. Per deserializzare correttamente il mio tipo, avevo bisogno di impostare la proprietà Converter quando il contratto è per il mio tipo.

Ecco il codice (con la gestione degli errori/ecc rimosso per mantenere le cose il più piccolo possibile).

In primo luogo, il tipo che ha bisogno di un trattamento speciale:

public struct Optional<T> 
{ 
    public readonly bool ValueProvided; 
    public readonly T Value; 

    private Optional(T value) 
    { 
     this.ValueProvided = true; 
     this.Value = value; 
    } 

    public static implicit operator Optional<T>(T value) 
    { 
     return new Optional<T>(value); 
    } 
} 

E c'è il convertitore che serializzare correttamente dopo sappiamo che dovrebbe essere serializzato:

public class OptionalJsonConverter<T> : JsonConverter 
{ 
    public static OptionalJsonConverter<T> Instance = new OptionalJsonConverter<T>(); 

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 
    { 
     var optional = (Optional<T>)value; // Cast so we can access the Optional<T> members 
     serializer.Serialize(writer, optional.Value); 
    } 

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 
    { 
     var valueType = objectType.GetGenericArguments()[ 0 ]; 
     var innerValue = (T)serializer.Deserialize(reader, valueType); 
     return (Optional<T>)innerValue; // Explicitly invoke the conversion from T to Optional<T> 
    } 

    public override bool CanConvert(Type objectType) 
    { 
     return objectType == typeof(Optional<T>); 
    } 
} 

Infine, e soprattutto -verbosamente, ecco il ContractResolver che inserisce i ganci:

public class CustomContractResolver : DefaultContractResolver 
{ 
    // For deserialization. Detect when the type is being deserialized and set the converter for it. 
    public override JsonContract ResolveContract(Type type) 
    { 
     var contract = base.ResolveContract(type); 
     if(contract.Converter == null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) 
     { 
      // This may look fancy but it's just calling GetOptionalJsonConverter<T> with the correct T 
      var optionalValueType = type.GetGenericArguments()[ 0 ]; 
      var genericMethod = this.GetAndMakeGenericMethod("GetOptionalJsonConverter", optionalValueType); 
      var converter = (JsonConverter)genericMethod.Invoke(null, null); 
      // Set the converter for the type 
      contract.Converter = converter; 
     } 
     return contract; 
    } 

    public static OptionalJsonConverter<T> GetOptionalJsonConverter<T>() 
    { 
     return OptionalJsonConverter<T>.Instance; 
    } 

    // For serialization. Detect when we're creating a JsonProperty for an Optional<T> member and modify it accordingly. 
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) 
    { 
     var jsonProperty = base.CreateProperty(member, memberSerialization); 
     var type = jsonProperty.PropertyType; 
     if(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) 
     { 
      // This may look fancy but it's just calling SetJsonPropertyValuesForOptionalMember<T> with the correct T 
      var optionalValueType = type.GetGenericArguments()[ 0 ]; 
      var genericMethod = this.GetAndMakeGenericMethod("SetJsonPropertyValuesForOptionalMember", optionalValueType); 
      genericMethod.Invoke(null, new object[]{ member.Name, jsonProperty }); 
     } 
     return jsonProperty; 
    } 

    public static void SetJsonPropertyValuesForOptionalMember<T>(string memberName, JsonProperty jsonProperty) 
    { 
     if(jsonProperty.ShouldSerialize == null) // Honor ShouldSerialize* 
     { 
      jsonProperty.ShouldSerialize = 
       (declaringObject) => 
       { 
        if(jsonProperty.GetIsSpecified != null && jsonProperty.GetIsSpecified(declaringObject)) // Honor *Specified 
        { 
         return true; 
        }      
        object optionalValue; 
        if(!TryGetPropertyValue(declaringObject, memberName, out optionalValue) && 
         !TryGetFieldValue(declaringObject, memberName, out optionalValue)) 
        { 
         throw new InvalidOperationException("Better error message here"); 
        } 
        return ((Optional<T>)optionalValue).ValueProvided; 
       }; 
     } 
     if(jsonProperty.Converter == null) 
     { 
      jsonProperty.Converter = CustomContractResolver.GetOptionalJsonConverter<T>(); 
     } 
    } 

    // Utility methods used in this class 
    private MethodInfo GetAndMakeGenericMethod(string methodName, params Type[] typeArguments) 
    { 
     var method = this.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); 
     return method.MakeGenericMethod(typeArguments); 
    } 

    private static bool TryGetPropertyValue(object declaringObject, string propertyName, out object value) 
    { 
     var propertyInfo = declaringObject.GetType().GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); 
     if(propertyInfo == null) 
     { 
      value = null; 
      return false; 
     } 
     value = propertyInfo.GetValue(declaringObject, BindingFlags.GetProperty, null, null, null); 
     return true; 
    } 

    private static bool TryGetFieldValue(object declaringObject, string fieldName, out object value) 
    { 
     var fieldInfo = declaringObject.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); 
     if(fieldInfo == null) 
     { 
      value = null; 
      return false; 
     } 
     value = fieldInfo.GetValue(declaringObject); 
     return true; 
    } 
} 

Spero che aiuti qualcun altro. Sentiti libero di fare domande se qualcosa non è chiaro o se sembra che mi sia sfuggito qualcosa.

+0

Apprezzo che tu abbia postato questo, ma non posso credere che NewtonSoft l'abbia reso così complicato! Tutto ciò di cui avevamo bisogno era una ShouldSerialize che fosse anche passata nel valore, quindi nessuna riflessione sarebbe stata necessaria. –

+0

Ho appena postato una richiesta di funzionalità qui https://github.com/JamesNK/Newtonsoft.Json/issues/1488 –

4

Poiché si richiede di farlo in base al valore di una proprietà, ciò che si può fare è mettere i dati in un dizionario. È possibile escludere l'aggiunta del valore al dizionario. Di seguito è riportato un semplice esempio di come ottenere i dati da un oggetto.

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

[TestFixture] 
public class Tests 
{ 
    [Test] 
    public void ConvertTest() 
    { 
     var dictionary = new Dictionary<string, object>(); 
     var @class = new Class1 { Name = "Joe" }; 

     var propertyInfos = typeof (Class1).GetProperties(); 

     foreach (PropertyInfo propertyInfo in propertyInfos) 
     { 
      dictionary.Add(propertyInfo.Name, propertyInfo.GetValue(@class, BindingFlags.GetProperty, null, null, null)); 
     } 

     var serializeObject = JsonConvert.SerializeObject(dictionary); 
     var o = JsonConvert.SerializeObject(@class); 

     Console.WriteLine(serializeObject); 
     Console.WriteLine(o); 

     var class1 = JsonConvert.DeserializeObject<Class1>(serializeObject); 
     Console.WriteLine(class1.Name); 
    } 
} 
+1

Il fatto che il tuo codice sembri un test unitario senza che quasi nessuno mi abbia fatto perdere l'eccellente punto che stai facendo. Non so se risolverebbe la domanda OP, ma i miei test iniziali sembrano dimostrare che ha risolto il mio problema che era vicino agli OP. –

Problemi correlati