Json.NET序列化有没有办法区分"null because not present"和"null because null"?
Is there a way in Json.NET serialization to distinguish between "null because not present" and "null because null"?
我在 ASP.NET webapi 代码库中工作,我们严重依赖自动支持 JSON 通过 JSON.NET.[=16 将消息主体反序列化为 .NET 对象=]
作为为我们的一项资源构建补丁支持的一部分,我非常想区分 JSON 对象中不存在的可选 属性 与相同的 属性 属性 明确为 null。我的意图是将第一个用于 "don't change what's there" 与 "delete this thing."
有谁知道是否可以标记我的 C# DTO,以便在它们被反序列化时 JSON.NET 可以告诉我它是哪种情况?现在它们只是显示为 null,我不知道为什么。
相反,如果有人能想出一个更好的设计,不需要我这样做,同时仍然支持补丁动词,我很想听听你的建议。
作为一个具体示例,请考虑将传递给 put 的有效载荷:
{
"field1": "my field 1",
"nested": {
"nested1": "something",
"nested2": "else"
}
}
现在,如果我只想更新 field1,我应该可以将其作为 HTTP 补丁发送:
{
"field1": "new field1 value"
}
并且嵌套值将保持不变。但是,如果我发送这个:
{
"nested": null
}
我想知道这意味着我应该明确删除嵌套数据。
您可以向 JSON 对象和(最有可能的)DTO 添加一些元数据。它需要额外的处理,但非常透明并且明确地完成了您需要的(假设您可以命名新字段,这样您就知道它不会与实际数据冲突)。
{
"deletedItems": null,
"field1": "my field 1",
"nested": {
"deletedItems": null,
"nested1": "something",
"nested2": "else"
}
}
{
"deletedItems": "nested",
"field1": "new value",
"nested": null
}
或者,如果您的对象模型能更好地适应每个字段,您可以添加一个 "isDeleted" 属性,但这听起来比删除字段列表要多得多。
如果你使用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");
}
Fiddle: https://dotnetfiddle.net/VJO7ay
更新
如果您使用 Web 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
returns false,转换器将不会被使用在序列化期间。)
现在,要在 Web API 中使用转换器,您需要将其插入到配置中。将此添加到您的 Application_Start()
方法中:
var config = GlobalConfiguration.Configuration;
var jsonSettings = config.Formatters.JsonFormatter.SerializerSettings;
jsonSettings.Converters.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<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
我不想劫持这个问题,但我在这里发布了一个稍微不同的方法来解决这个问题:。
方法是将可反序列化类型中的字段替换为一个结构,该结构将通过 IsSet 属性.
自动跟踪值(甚至为 null)
我突然想到了最优雅的解决方案:
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace testJsonDeserializer
{
class Program
{
static void Main(string[] args)
{
// this operator has the password set to meow.
Operator originalOperator = new Operator
{
OperatorGuid = Guid.Parse("3bb1dc84-2963-4921-a567-fb2e7475623d"),
UserName = "noortje@peterhuppertz.net",
Password = "meow",
PropertyThatWillBeNulled = "noortje@peterhuppertz.net",
};
// this json EXPLICITLY sets the PropertyThatWillBeNulled to null, but omits the Password property, making it null IMPLICITLY.
string json =
"{ \"OperatorGuid\":\"3bb1dc84-2963-4921-a567-fb2e7475623d\", \"UserName\": \"noortje@peterhuppertz.net\", \"Email\": null }";
// What a PATCH would want for the target object is to leave implicit Nulls unchanged, but explicit nulls set to null.
Operator patchedOperator = JsonConvert.DeserializeObject<Operator>(json);
// At this stage, our patched operator has the password set to null. We do not want that; we want to keep whatever is stored in originalOperator
Operator opToStore = MapJsonToOperator(patchedOperator, originalOperator, json);
Console.WriteLine("Our patched operator:");
Console.WriteLine($"Guid: {opToStore.OperatorGuid}");
Console.WriteLine($"UserName: {opToStore.UserName}");
Console.WriteLine($"Password: {opToStore.Password}");
Console.WriteLine($"Email: {opToStore.PropertyThatWillBeNulled}");
Console.ReadKey();
}
private static Operator MapJsonToOperator(Operator source, Operator original, string json)
{
Operator result = new Operator
{
OperatorGuid = source.OperatorGuid,
UserName = source.UserName != null
// we check if the source property has a value, if so, we use that value.
? source.UserName
// if it doesn't, we check the Json to see if the value is in there, explicitly set to NULL. If it is, we set it to NULL as well
: (IsNullValueExplicit(json, "UserName") ? null
// if it is not in the json (making it implicitly null), we just leave the value as it was.
: original.UserName),
PropertyThatWillBeNulled = source.PropertyThatWillBeNulled != null
? source.PropertyThatWillBeNulled
: (IsNullValueExplicit(json, "Email") ? null : original.PropertyThatWillBeNulled),
Password = source.Password != null
? source.Password
: (IsNullValueExplicit(json, "Password") ? null : original.Password),
};
return result;
}
static bool IsNullValueExplicit(string json, string fieldName)
{
JToken outer = JToken.Parse(json);
JObject inner = outer.Value<JObject>();
List<string> keys = inner.Properties().Select(p => p.Name).ToList();
return keys.Contains(fieldName);
}
}
public class Operator
{
public Guid OperatorGuid { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string PropertyThatWillBeNulled { get; set; }
}
}
我知道,这里有很多评论。也许我解释过度了……但我想我宁可谨慎行事。
查看 Json.NET 源代码,我发现它支持填充带有 "Specified" 后缀的 bool 属性,以指示 属性 是否包含在数据中:
class MyClass
{
public string Field1 { get; set; }
public Nested Nested { get; set; }
public bool NestedSpecified { get; set; }
}
class Nested
{
public string Nested1 { get; set; }
public string Nested2 { get; set; }
}
输入:
{
"field1": "my field 1",
"nested": {
"nested1": "something",
"nested2": "else"
}
}
结果实例:
MyClass { Field1="my field 1", Nested=Nested { Nested1="something", Nested2="else" }, NestedSpecified=true }
输入:
{
"field1": "new field1 value"
}
结果实例:
MyClass { Field1="new field1 value", Nested=null, NestedSpecified=false }
输入:
{
"nested": null
}
结果实例:
MyClass { Field1=null, Nested=null, NestedSpecified=true }
我在 Json.NET 文档中找不到此功能,但看起来它已经存在 since 2010。
我在 ASP.NET webapi 代码库中工作,我们严重依赖自动支持 JSON 通过 JSON.NET.[=16 将消息主体反序列化为 .NET 对象=]
作为为我们的一项资源构建补丁支持的一部分,我非常想区分 JSON 对象中不存在的可选 属性 与相同的 属性 属性 明确为 null。我的意图是将第一个用于 "don't change what's there" 与 "delete this thing."
有谁知道是否可以标记我的 C# DTO,以便在它们被反序列化时 JSON.NET 可以告诉我它是哪种情况?现在它们只是显示为 null,我不知道为什么。
相反,如果有人能想出一个更好的设计,不需要我这样做,同时仍然支持补丁动词,我很想听听你的建议。
作为一个具体示例,请考虑将传递给 put 的有效载荷:
{
"field1": "my field 1",
"nested": {
"nested1": "something",
"nested2": "else"
}
}
现在,如果我只想更新 field1,我应该可以将其作为 HTTP 补丁发送:
{
"field1": "new field1 value"
}
并且嵌套值将保持不变。但是,如果我发送这个:
{
"nested": null
}
我想知道这意味着我应该明确删除嵌套数据。
您可以向 JSON 对象和(最有可能的)DTO 添加一些元数据。它需要额外的处理,但非常透明并且明确地完成了您需要的(假设您可以命名新字段,这样您就知道它不会与实际数据冲突)。
{
"deletedItems": null,
"field1": "my field 1",
"nested": {
"deletedItems": null,
"nested1": "something",
"nested2": "else"
}
}
{
"deletedItems": "nested",
"field1": "new value",
"nested": null
}
或者,如果您的对象模型能更好地适应每个字段,您可以添加一个 "isDeleted" 属性,但这听起来比删除字段列表要多得多。
如果你使用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");
}
Fiddle: https://dotnetfiddle.net/VJO7ay
更新
如果您使用 Web 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
returns false,转换器将不会被使用在序列化期间。)
现在,要在 Web API 中使用转换器,您需要将其插入到配置中。将此添加到您的 Application_Start()
方法中:
var config = GlobalConfiguration.Configuration;
var jsonSettings = config.Formatters.JsonFormatter.SerializerSettings;
jsonSettings.Converters.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<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
我不想劫持这个问题,但我在这里发布了一个稍微不同的方法来解决这个问题:。
方法是将可反序列化类型中的字段替换为一个结构,该结构将通过 IsSet 属性.
自动跟踪值(甚至为 null)我突然想到了最优雅的解决方案:
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace testJsonDeserializer
{
class Program
{
static void Main(string[] args)
{
// this operator has the password set to meow.
Operator originalOperator = new Operator
{
OperatorGuid = Guid.Parse("3bb1dc84-2963-4921-a567-fb2e7475623d"),
UserName = "noortje@peterhuppertz.net",
Password = "meow",
PropertyThatWillBeNulled = "noortje@peterhuppertz.net",
};
// this json EXPLICITLY sets the PropertyThatWillBeNulled to null, but omits the Password property, making it null IMPLICITLY.
string json =
"{ \"OperatorGuid\":\"3bb1dc84-2963-4921-a567-fb2e7475623d\", \"UserName\": \"noortje@peterhuppertz.net\", \"Email\": null }";
// What a PATCH would want for the target object is to leave implicit Nulls unchanged, but explicit nulls set to null.
Operator patchedOperator = JsonConvert.DeserializeObject<Operator>(json);
// At this stage, our patched operator has the password set to null. We do not want that; we want to keep whatever is stored in originalOperator
Operator opToStore = MapJsonToOperator(patchedOperator, originalOperator, json);
Console.WriteLine("Our patched operator:");
Console.WriteLine($"Guid: {opToStore.OperatorGuid}");
Console.WriteLine($"UserName: {opToStore.UserName}");
Console.WriteLine($"Password: {opToStore.Password}");
Console.WriteLine($"Email: {opToStore.PropertyThatWillBeNulled}");
Console.ReadKey();
}
private static Operator MapJsonToOperator(Operator source, Operator original, string json)
{
Operator result = new Operator
{
OperatorGuid = source.OperatorGuid,
UserName = source.UserName != null
// we check if the source property has a value, if so, we use that value.
? source.UserName
// if it doesn't, we check the Json to see if the value is in there, explicitly set to NULL. If it is, we set it to NULL as well
: (IsNullValueExplicit(json, "UserName") ? null
// if it is not in the json (making it implicitly null), we just leave the value as it was.
: original.UserName),
PropertyThatWillBeNulled = source.PropertyThatWillBeNulled != null
? source.PropertyThatWillBeNulled
: (IsNullValueExplicit(json, "Email") ? null : original.PropertyThatWillBeNulled),
Password = source.Password != null
? source.Password
: (IsNullValueExplicit(json, "Password") ? null : original.Password),
};
return result;
}
static bool IsNullValueExplicit(string json, string fieldName)
{
JToken outer = JToken.Parse(json);
JObject inner = outer.Value<JObject>();
List<string> keys = inner.Properties().Select(p => p.Name).ToList();
return keys.Contains(fieldName);
}
}
public class Operator
{
public Guid OperatorGuid { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string PropertyThatWillBeNulled { get; set; }
}
}
我知道,这里有很多评论。也许我解释过度了……但我想我宁可谨慎行事。
查看 Json.NET 源代码,我发现它支持填充带有 "Specified" 后缀的 bool 属性,以指示 属性 是否包含在数据中:
class MyClass
{
public string Field1 { get; set; }
public Nested Nested { get; set; }
public bool NestedSpecified { get; set; }
}
class Nested
{
public string Nested1 { get; set; }
public string Nested2 { get; set; }
}
输入:
{
"field1": "my field 1",
"nested": {
"nested1": "something",
"nested2": "else"
}
}
结果实例:
MyClass { Field1="my field 1", Nested=Nested { Nested1="something", Nested2="else" }, NestedSpecified=true }
输入:
{
"field1": "new field1 value"
}
结果实例:
MyClass { Field1="new field1 value", Nested=null, NestedSpecified=false }
输入:
{
"nested": null
}
结果实例:
MyClass { Field1=null, Nested=null, NestedSpecified=true }
我在 Json.NET 文档中找不到此功能,但看起来它已经存在 since 2010。