一尘不染

Json.NET序列化中是否有一种方法可以区分“因为不存在而为空”和“因为没有而为空”?

json

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

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


阅读 165

收藏
2020-07-27

共1个答案

一尘不染

如果使用Json.Net的LINQ-to-JSON
API
(JTokens,JObjects等)来解析JSON,则可以分辨出JSON
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上需要有一个位置来存储字段状态。我建议使用像这样的基于字典的方案:

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
{
    ...
}

有了转换器基础结构之后,您可以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

2020-07-27