2015-03-13 13 views
12

Sto lavorando in una base di codice webapi ASP.NET in cui ci basiamo molto sul supporto automatico per la deserializzazione JSON di corpi di messaggi in oggetti .NET tramite JSON.NET.C'è un modo nella serializzazione Json.NET per distinguere tra "null perché non presente" e "null perché null"?

Come parte del supporto di patch per una delle nostre risorse, mi piacerebbe molto distinguere tra una proprietà facoltativa nell'oggetto JSON che non è presente, contro quella stessa proprietà che è esplicitamente a null. La mia intenzione è quella di usare il primo per "non cambiare ciò che è lì" contro "elimina questa cosa".

Qualcuno sa se è possibile contrassegnare i miei DTO C# in modo che quando vengono deserializzati, JSON.NET può dirmi in quale caso si trattava? In questo momento sono appena arrivati ​​come nulli, e non posso dire perché.

Viceversa, se qualcuno riesce a trovare un progetto migliore che non mi impone di farlo in questo modo mentre ancora supporta il verbo di patch, mi piacerebbe sentire la tua proposta.

Come esempio concreto, si consideri questo payload che potrebbe essere trasferito a mettere:

{ 
    "field1": "my field 1", 
    "nested": { 
    "nested1": "something", 
    "nested2": "else" 
    } 
} 

Ora, se volevo solo aggiornare field1, dovrei essere in grado di inviare questo come una patch HTTP:

{ 
    "field1": "new field1 value" 
} 

e i valori nidificati rimarrebbero intatti. Tuttavia, se ho inviato questo:

{ 
    "nested": null 
} 

Voglio sapere questo significa che dovrei rimuovere esplicitamente i dati nidificati.

+0

Interessante. Contratti dati WCF/'EmiteDefaultValue' applicati a WebAPI ... Buona chiamata. –

risposta

14

Se si utilizza di Json.NET LINQ-to-JSON API (JTokens, JObjects, ecc) per analizzare il JSON, si può dire la differenza tra un valore null e un campo che semplicemente non esiste in JSON. Per esempio:

JToken root = JToken.Parse(json); 

JToken nested = root["nested"]; 
if (nested != null) 
{ 
    if (nested.Type == JTokenType.Null) 
    { 
     Console.WriteLine("nested is set to null"); 
    } 
    else 
    { 
     Console.WriteLine("nested has a value: " + nested.ToString()); 
    } 
} 
else 
{ 
    Console.WriteLine("nested does not exist"); 
} 

Fiddle: https://dotnetfiddle.net/VJO7ay

UPDATE

Se stai deserializzazione in oggetti concreti utilizzando API Web, è comunque possibile utilizzare il concetto di cui sopra con la creazione di un custom JsonConverter per gestire i tuoi DTO. Il problema è che deve esserci un posto nei DTO per memorizzare lo stato del campo durante la deserializzazione. Io suggerirei di usare uno schema basato su dizionario in questo modo:

enum FieldDeserializationStatus { WasNotPresent, WasSetToNull, HasValue } 

interface IHasFieldStatus 
{ 
    Dictionary<string, FieldDeserializationStatus> FieldStatus { get; set; } 
} 

class FooDTO : IHasFieldStatus 
{ 
    public string Field1 { get; set; } 
    public BarDTO Nested { get; set; } 
    public Dictionary<string, FieldDeserializationStatus> FieldStatus { get; set; } 
} 

class BarDTO : IHasFieldStatus 
{ 
    public int Num { get; set; } 
    public string Str { get; set; } 
    public bool Bool { get; set; } 
    public decimal Dec { get; set; } 
    public Dictionary<string, FieldDeserializationStatus> FieldStatus { get; set; } 
} 

Il convertitore personalizzato sarebbe poi utilizzare sopra tecnica LINQ to JSON per leggere il JSON per l'oggetto deserializzato.Per ogni campo nell'oggetto di destinazione, aggiungerebbe un elemento al dizionario FieldStatus di quell'oggetto che indica se il campo aveva un valore, era esplicitamente impostato su null o non esisteva nel JSON. Ecco ciò che il codice potrebbe essere simile:

class DtoConverter : JsonConverter 
{ 
    public override bool CanConvert(Type objectType) 
    { 
     return (objectType.IsClass && 
       objectType.GetInterfaces().Any(i => i == typeof(IHasFieldStatus))); 
    } 

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 
    { 
     var jsonObj = JObject.Load(reader); 
     var targetObj = (IHasFieldStatus)Activator.CreateInstance(objectType); 

     var dict = new Dictionary<string, FieldDeserializationStatus>(); 
     targetObj.FieldStatus = dict; 

     foreach (PropertyInfo prop in objectType.GetProperties()) 
     { 
      if (prop.CanWrite && prop.Name != "FieldStatus") 
      { 
       JToken value; 
       if (jsonObj.TryGetValue(prop.Name, StringComparison.OrdinalIgnoreCase, out value)) 
       { 
        if (value.Type == JTokenType.Null) 
        { 
         dict.Add(prop.Name, FieldDeserializationStatus.WasSetToNull); 
        } 
        else 
        { 
         prop.SetValue(targetObj, value.ToObject(prop.PropertyType, serializer)); 
         dict.Add(prop.Name, FieldDeserializationStatus.HasValue); 
        } 
       } 
       else 
       { 
        dict.Add(prop.Name, FieldDeserializationStatus.WasNotPresent); 
       } 
      } 
     } 

     return targetObj; 
    } 

    public override bool CanWrite 
    { 
     get { return false; } 
    } 

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 
    { 
     throw new NotImplementedException(); 
    } 
} 

Il convertitore sopra funziona su qualsiasi oggetto che implementa l'interfaccia IHasFieldStatus. (Notare che non è necessario implementare il metodo WriteJson nel convertitore a meno che non si intenda fare qualcosa di personalizzato anche sulla serializzazione. Poiché CanWrite restituisce false, il convertitore non verrà utilizzato durante la serializzazione.)

Ora, da utilizzare il convertitore in Web API, è necessario inserirlo nella configurazione. Aggiungi questo al tuo metodo Application_Start():

var config = GlobalConfiguration.Configuration; 
var jsonSettings = config.Formatters.JsonFormatter.SerializerSettings; 
jsonSettings.C‌​onverters.Add(new DtoConverter()); 

Se si preferisce, è possibile decorare ogni DTO con un attributo [JsonConverter] come questo invece di impostare il convertitore nella configurazione globale:

[JsonConverter(typeof(DtoConverter))] 
class FooDTO : IHasFieldStatus 
{ 
    ... 
} 

Con l'infrastruttura convertitore sul posto, è quindi possibile interrogare il dizionario FieldStatus sul DTO dopo la deserializzazione per vedere cosa è successo per un particolare campo. Ecco una demo completa (console app):

public class Program 
{ 
    public static void Main() 
    { 
     ParseAndDump("First run", @"{ 
      ""field1"": ""my field 1"", 
      ""nested"": { 
       ""num"": null, 
       ""str"": ""blah"", 
       ""dec"": 3.14 
      } 
     }"); 

     ParseAndDump("Second run", @"{ 
      ""field1"": ""new field value"" 
     }"); 

     ParseAndDump("Third run", @"{ 
      ""nested"": null 
     }"); 
    } 

    private static void ParseAndDump(string comment, string json) 
    { 
     Console.WriteLine("--- " + comment + " ---"); 

     JsonSerializerSettings settings = new JsonSerializerSettings(); 
     settings.Converters.Add(new DtoConverter()); 

     FooDTO foo = JsonConvert.DeserializeObject<FooDTO>(json, settings); 

     Dump(foo, ""); 

     Console.WriteLine(); 
    } 

    private static void Dump(IHasFieldStatus dto, string indent) 
    { 
     foreach (PropertyInfo prop in dto.GetType().GetProperties()) 
     { 
      if (prop.Name == "FieldStatus") continue; 

      Console.Write(indent + prop.Name + ": "); 
      object val = prop.GetValue(dto); 
      if (val is IHasFieldStatus) 
      { 
       Console.WriteLine(); 
       Dump((IHasFieldStatus)val, " "); 
      } 
      else 
      { 
       FieldDeserializationStatus status = dto.FieldStatus[prop.Name]; 
       if (val != null) 
        Console.Write(val.ToString() + " "); 
       if (status != FieldDeserializationStatus.HasValue) 
        Console.Write("(" + status + ")"); 
       Console.WriteLine(); 
      } 
     } 
    } 
} 

uscita:

--- First run --- 
Field1: my field 1 
Nested: 
    Num: 0 (WasSetToNull) 
    Str: blah 
    Bool: False (WasNotPresent) 
    Dec: 3.14 

--- Second run --- 
Field1: new field value 
Nested: (WasNotPresent) 

--- Third run --- 
Field1: (WasNotPresent) 
Nested: (WasSetToNull) 

Fiddle: https://dotnetfiddle.net/xyKrg2

+0

Purtroppo non è un'opzione. In questo momento stiamo utilizzando il binding automatico dei parametri WebAPI/materiale di associazione modello, che serializza automaticamente e deserializza i tipi per noi. Il nostro architetto di progetto non vuole rinunciare all'affidabilità della serializzazione garantita senza un motivo significativo per il fallimento dello show. Questo non è uno, sfortunatamente. –

+1

Ho aggiornato la mia risposta con una soluzione che dovrebbe funzionare per Web API. Spero che questo ti aiuti. –

+0

Bello! Avevo iniziato a pensare a qualcosa di simile a me stesso. –

2

È possibile aggiungere alcuni metadati ai propri oggetti JSON e (molto probabilmente) DTO. Richiederebbe un'ulteriore elaborazione, ma è piuttosto trasparente e realizza in modo inequivocabile ciò di cui hai bisogno (assumendo che tu possa nominare il nuovo campo in modo tale che tu sappia che non entrerà in collisione con i dati reali).

{ 
    "deletedItems": null, 
    "field1": "my field 1", 
    "nested": { 
    "deletedItems": null, 
    "nested1": "something", 
    "nested2": "else" 
    } 
} 
{ 
    "deletedItems": "nested", 
    "field1": "new value", 
    "nested": null 
} 

In alternativa, è possibile aggiungere una proprietà "isDeleted" per campo se il modello a oggetti accomoda che meglio, ma che suona come molto più lavoro che un elenco di campi eliminati.

+0

Se segui questo percorso, ti consiglio di usare '__fieldName' per impedire (meglio che puoi) di scontrarsi con una proprietà esistente. –

0

Non voglio dirottare questa domanda, ma ho postato un approccio leggermente diverso per questo problema qui: https://stackoverflow.com/a/31489835/1395758.

L'approccio è sostituire i campi nel tipo deserializable con una struttura che terrà automaticamente traccia dei valori (anche null) tramite una proprietà IsSet.

Problemi correlati