我在ASP.NET Webapi代码库中工作,我们在很大程度上依赖对通过JSON.NET将消息主体从JSON反序列化为.NET对象的自动支持。
作为构建对我们资源之一的补丁程序支持的一部分,我非常想区分JSON对象中不存在的可选属性和显式为null的相同属性。我的意图是将第一个用于“不要更改其中的内容”还是“删除此内容”。
有谁知道是否可以标记我的C#DTO,以便在对它们进行反序列化时JSON.NET可以告诉我是哪种情况?现在,它们只是空值,我不知道为什么。
相反,如果任何人都可以提出一个更好的设计,而该设计不需要我在仍支持补丁动词的情况下以这种方式进行操作,那么我很想听听您的建议。
作为一个具体示例,请考虑将要传递的有效负载:
{ "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,则可以分辨出JSON null中根本不存在的值和字段之间的区别。例如:
null
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
更新
如果要使用Web API反序列化为具体对象,则仍可以通过创建自定义JsonConverter来处理DTO来使用上述概念。问题是在反序列化期间,您的DTO上需要有一个位置来存储字段状态。我建议使用像这样的基于字典的方案:
JsonConverter
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中不存在。代码如下所示:
FieldStatus
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,因此在序列化期间将不使用转换器。)
IHasFieldStatus
WriteJson
CanWrite
现在,要在Web API中使用转换器,您需要将其插入配置中。将此添加到您的Application_Start()方法:
Application_Start()
var config = GlobalConfiguration.Configuration; var jsonSettings = config.Formatters.JsonFormatter.SerializerSettings; jsonSettings.Converters.Add(new DtoConverter());
如果愿意,可以用这样的[JsonConverter]属性装饰每个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)
小提琴:https : //dotnetfiddle.net/xyKrg2