【问题标题】:Is there a way in Json.NET serialization to distinguish between "null because not present" and "null because null"?Json.NET 序列化中有没有办法区分“null 因为不存在”和“null 因为 null”?
【发布时间】:2015-05-15 11:10:56
【问题描述】:

我在一个 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
}

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

【问题讨论】:

  • 有趣。 WCF 数据协定/EmiteDefaultValue 应用于 WebAPI...很好。

标签: c# asp.net json asp.net-web-api json.net


【解决方案1】:

如果您使用 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

更新

如果您使用 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 返回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<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

【讨论】:

  • 不幸的是,这不是一个选项。现在我们正在使用自动 WebAPI 参数绑定/模型绑定的东西,它会自动为我们序列化和反序列化类型。我们的项目架构师不想在没有显着阻碍原因的情况下放弃保证序列化的可靠性。不幸的是,这不是一个。
  • 我已经用应该适用于 Web API 的解决方案更新了我的答案。希望这会有所帮助。
  • 不错!我自己也开始考虑类似的事情。
【解决方案2】:

查看Json.NET源码,发现它支持填充bool属性,后缀为“Specified”,表示该属性是否包含在数据中:

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

【讨论】:

  • 这是一个不错的发现。我为 Json.Net 工作了多年,我不知道有这个功能。感谢您发布! +1
  • 哇,我也不知道有这个!如果它长时间没有记录,我不确定我是否想依赖它,这让我想知道它有什么问题。 :-)
  • 我在发布后测试时确实发现了它的一个限制 - 它不能很好地与构造函数参数一起使用。在上面的示例中,如果 nestednestedSpecifiedMyClass 的构造函数参数,则该值将始终为 false。
【解决方案3】:

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

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

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

【讨论】:

  • 如果你真的走这条路,我建议使用__fieldName 来防止(尽你所能)与现有财产发生冲突。
【解决方案4】:

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

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

【讨论】:

    【解决方案5】:

    我突然想到的最优雅的解决方案:

    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; }
        }
    }
    

    我知道,这里有很多cmets。也许我过度解释了......但我认为我会在谨慎方面犯错。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-07-18
      • 1970-01-01
      • 2017-04-02
      • 2014-01-26
      • 2014-09-05
      相关资源
      最近更新 更多