在Json.NET序列化中有没有办法区分“null因为不存在”和“null因为null”?

我正在使用ASP.NET webapi代码库,我们在很大程度上依赖于通过JSON.NET自动支持将消息体的JSON反序列化为.NET对象。

作为构建我们的一个资源的补丁支持的一部分,我非常希望区分JSON对象中不存在的可选属性与明确为null的相同属性。 我的意图是使用第一个“不要改变那里有什么”与“删除这个东西”。

有没有人知道是否有可能标记我的C#DTO,这样当它们被反序列化时,JSON.NET可以告诉我它是哪种情况? 现在他们只是空出来,我不知道为什么。

相反,如果有人能够提出一个更好的设计,不需要我这样做,同时仍然支持补丁动词,我很想听听你的建议。

作为一个具体的例子,考虑将传递给put的这个有效负载:

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

现在,如果我只想更新field1,我应该能够将其作为HTTP补丁发送:

 { "field1": "new field1 value" } 

并且嵌套值将保持不变。 但是,如果我发送了这个:

 { "nested": null } 

我想知道这意味着我应该明确删除嵌套数据。

如果您使用Json.Net的LINQ-to-JSON API (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"); } 

小提琴: https : //dotnetfiddle.net/VJO7ay

UPDATE

如果您使用Web API反序列化为具体对象,您仍然可以通过创建自定义JsonConverter来处理您的DTO,从而使用上述概念。 问题是,在反序列化期间,您的DTO上需要有一个位置来存储字段状态。 我建议使用这样的基于字典的方案:

 enum FieldDeserializationStatus { WasNotPresent, WasSetToNull, HasValue } interface IHasFieldStatus { Dictionary FieldStatus { get; set; } } class FooDTO : IHasFieldStatus { public string Field1 { get; set; } public BarDTO Nested { get; set; } public Dictionary 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 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(); 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,因此在序列化期间不会使用转换器。)

现在,要在Web API中使用转换器,您需要将其插入到配置中。 将其添加到Application_Start()方法:

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

如果您愿意,可以使用[JsonConverter]属性来装饰每个DTO,而不是在全局配置中设置转换器:

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

使用转换器基础结构后,您可以在反序列化后查询DTO上的FieldStatus字典,以查看任何特定字段发生的情况。 这是一个完整的演示(控制台应用程序):

 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(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) 

小提琴: https : //dotnetfiddle.net/xyKrg2

您可以向JSON对象和(最有可能的)DTO添加一些元数据。 它需要额外的处理,但是非常透明,并且明确地完成了你需要的东西(假设你可以命名新字段,以便你知道它不会与实际数据冲突)。

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

或者,如果对象模型更好地适应,则可以为每个字段添加“isDeleted”属性,但这听起来比删除字段列表要多得多。

我不想劫持这个问题,但我在这里发布了一个略有不同的解决方法: https : //stackoverflow.com/a/31489835/1395758 。

方法是使用结构替换可反序列化类型中的字段,该结构将通过IsSet属性自动跟踪值(甚至为null)。