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.Converters.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
Interessante. Contratti dati WCF/'EmiteDefaultValue' applicati a WebAPI ... Buona chiamata. –