Json.NET 序列化中是否有办法区分“null 因为不存在"?和“空,因为空"?

时间:2023-04-26
本文介绍了Json.NET 序列化中是否有办法区分“null 因为不存在"?和“空,因为空"?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着跟版网的小编来一起学习吧!

问题描述

我在一个 ASP.NET webapi 代码库中工作,我们严重依赖通过 JSON.NET 将消息体的 JSON 反序列化为 .NET 对象的自动支持.

I'm working in an ASP.NET webapi codebase where we rely heavily on the automatic support for JSON deserialization of message bodies into .NET objects via JSON.NET.

作为为我们的资源之一构建补丁支持的一部分,我非常想区分 JSON 对象中不存在的可选属性与明确为 null 的相同属性.我的意图是将第一个用于不要改变那里的东西"与删除这个东西".

As part of building out patch support for one of our resources, I'd very much like to distinguish between an optional property in the JSON object that's not present, vs. that same property that's explicitly to null. My intention is to use the first for "don't change what's there" vs. "delete this thing."

有谁知道是否可以标记我的 C# DTO,以便在反序列化它们时 JSON.NET 可以告诉我是哪种情况?现在它们只是作为空值出现,我不知道为什么.

Does anyone know if it's possible to mark up my C# DTOs so that when they're deserialized that JSON.NET can tell me which case it was? Right now they're just come up as null, and I can't tell why.

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

Conversely, if anyone can come up with a better design that doesn't require me to do it this way while still supporting the patch verb, I'd love to hear your proposal.

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

As a concrete example, consider this payload that would be passed to put:

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

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

Now, if I just wanted to update field1, I should be able to send this as an HTTP patch:

{
  "field1": "new field1 value"
}

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

and the nested values would remain untouched. However, if I sent this:

{
  "nested": null
}

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

I want to know this means I should explicitly remove the nested data.

推荐答案

如果你使用Json.Net的LINQ-to-JSON API(JTokens、JObjects 等)解析 JSON,您可以区分 null 值和 JSON 中根本不存在的字段.例如:

If you use Json.Net's LINQ-to-JSON API (JTokens, JObjects, etc.) to parse the JSON, you can tell the difference between a null value and a field that simply doesn't exist in the JSON. For example:

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 上需要有一个位置来存储反序列化期间的字段状态.我建议使用这样的基于字典的方案:

If you're deserializing into concrete objects using Web API, you can still use the above concept by creating a custom JsonConverter to handle your DTOs. The catch is that there needs to be a place on your DTOs to store the field status during deserialization. I would suggest using a dictionary-based scheme like this:

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 中不存在.下面是代码的样子:

The custom converter would then use above LINQ-to-JSON technique to read the JSON for the object being deserialized. For each field in the target object, it would add an item to that object's FieldStatus dictionary indicating whether the field had a value, was explicitly set to null or did not exist in the JSON. Here is what the code might look like:

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,转换器将不会在序列化期间使用.)

The above converter will work on any object that implements the IHasFieldStatus interface. (Note that you do not need to implement the WriteJson method in the converter unless you intend to do something custom on serialization as well. Since CanWrite returns false, the converter will not be used during serialization.)

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

Now, to use the converter in Web API, you need to insert it into the configuration. Add this to your Application_Start() method:

var config = GlobalConfiguration.Configuration;
var jsonSettings = config.Formatters.JsonFormatter.SerializerSettings;
jsonSettings.Converters.Add(new DtoConverter());

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

If you prefer, you can decorate each DTO with a [JsonConverter] attribute like this instead of setting the converter in the global config:

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

在转换器基础设施到位后,您可以在反序列化后查询 DTO 上的 FieldStatus 字典,以查看任何特定字段发生了什么.这是一个完整的演示(控制台应用程序):

With the converter infrastructure in place, you can then interrogate the FieldStatus dictionary on the DTO after deserialization to see what happened for any particular field. Here is a full demo (console app):

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

这篇关于Json.NET 序列化中是否有办法区分“null 因为不存在"?和“空,因为空"?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持跟版网!

上一篇:如何更改数字反序列化的默认类型? 下一篇:从 JsonReader 读取 JObject 时出错.当前 JsonReader 项不是对象:StartArray.小

相关文章