Ответ 1
Если вы используете Json.Net API LINQ-to-JSON (JTokens, JObjects и т.д.) для анализа JSON, вы можете определить разницу между null
и поле, которое просто не существует в JSON. Например:
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
Если вы десериализируетесь на конкретные объекты с помощью веб-API, вы все равно можете использовать эту концепцию, создав пользовательский JsonConverter
для обработки ваших DTO. Уловка заключается в том, что на ваших DTO должно быть место для хранения статуса поля во время десериализации. Я бы предложил использовать схему на основе словаря следующим образом:
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; }
}
Пользовательский конвертер затем будет использовать выше метод LINQ-to-JSON для чтения JSON для десериализованного объекта. Для каждого поля целевого объекта он добавит элемент к этому объекту FieldStatus
, который указывает, было ли поле имело значение, было явно установлено значение null или не существовало в JSON. Вот как выглядит код:
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();
}
}
Вышеуказанный конвертер будет работать на любом объекте, который реализует интерфейс IHasFieldStatus
. (Обратите внимание, что вам не нужно реализовывать метод WriteJson
в конвертере, если вы не намерены делать что-то обычное для сериализации. Поскольку CanWrite
возвращает false, конвертер не будет использоваться во время сериализации.)
Теперь, чтобы использовать конвертер в веб-API, вам нужно вставить его в конфигурацию. Добавьте это к вашему методу Application_Start()
:
var config = GlobalConfiguration.Configuration;
var jsonSettings = config.Formatters.JsonFormatter.SerializerSettings;
jsonSettings.Converters.Add(new DtoConverter());
Если вы предпочитаете, вы можете украсить каждый DTO атрибутом [JsonConverter]
, подобным этому, вместо настройки конвертера в глобальной конфигурации:
[JsonConverter(typeof(DtoConverter))]
class FooDTO : IHasFieldStatus
{
...
}
При наличии инфраструктуры конвертера вы можете затем опросить словарь FieldStatus
на DTO после десериализации, чтобы узнать, что произошло для какого-либо конкретного поля. Вот полное демо (консольное приложение):
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();
}
}
}
}
Вывод:
--- 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