JSON.NET 序列化 JObject,同时忽略空属性

JSON.NET serialize JObject while ignoring null properties

我有一个 JObject 用作调用 RESTful 网络服务的 模板 。这个 JObject 是通过解析器创建的,因为它被用作告诉用户端点模式是什么样子的模板,我必须想出一种方法来保留所有属性,这就是为什么我将它们的值默认为null。例如,这是对象最初的样子:

{  
   "Foo":{  
      "P1":null,
      "P2":null,
      "P3":null,
      "P4":{  
         "P1":null,
         "P2":null,
         "P3":null,
      },
      "FooArray":[  
         {  
            "F1":null,
            "F2":null,
            "F3":null,
         }
      ]
   },
   "Bar":null
}

然后用户可以根据需要填写各个字段,例如 Foo.P2Foo.P4.P1:

{  
   "Foo":{  
      "P1":null,
      "P2":"hello world",
      "P3":null,
      "P4":{  
         "P1":1,
         "P2":null,
         "P3":null,
      },
      "FooArray":[  
         {  
            "F1":null,
            "F2":null,
            "F3":null,
         }
      ]
   },
   "Bar":null
}

意味着他们只关心这两个领域。现在我想将此模板 (JObject) 序列化回 JSON 字符串,但只希望显示那些已填充的字段。所以我尝试了这个:

string json = JsonConvert.SerializeObject(template,
    new JsonSerializerSettings
    {
        NullValueHandling = NullValueHandling.Ignore
    });

不幸的是,这没有用。我遇到了 并意识到对象中的 null 值是实际的 JToken 类型而不是真正的 null,这是有道理的。但是,在这种非常特殊的情况下,我需要能够摆脱这些 "unused" 字段。我尝试手动遍历节点并删除它们,但这也不起作用。请注意,我使用的唯一托管类型是 JObject;我没有模型可以将对象转换为对象或在其上定义属性,因为这个 "template" 在运行时得到解析。我只是想知道是否有人遇到过这样的问题并且有任何见解。非常感谢任何帮助!

您可以使用如下所示的递归辅助方法在序列化之前从 JToken 层次结构中删除 null 值。

using System;
using Newtonsoft.Json.Linq;

public static class JsonHelper
{
    public static JToken RemoveEmptyChildren(JToken token)
    {
        if (token.Type == JTokenType.Object)
        {
            JObject copy = new JObject();
            foreach (JProperty prop in token.Children<JProperty>())
            {
                JToken child = prop.Value;
                if (child.HasValues)
                {
                    child = RemoveEmptyChildren(child);
                }
                if (!IsEmpty(child))
                {
                    copy.Add(prop.Name, child);
                }
            }
            return copy;
        }
        else if (token.Type == JTokenType.Array)
        {
            JArray copy = new JArray();
            foreach (JToken item in token.Children())
            {
                JToken child = item;
                if (child.HasValues)
                {
                    child = RemoveEmptyChildren(child);
                }
                if (!IsEmpty(child))
                {
                    copy.Add(child);
                }
            }
            return copy;
        }
        return token;
    }

    public static bool IsEmpty(JToken token)
    {
        return (token.Type == JTokenType.Null);
    }
}

演示:

string json = @"
{
    ""Foo"": {
        ""P1"": null,
        ""P2"": ""hello world"",
        ""P3"": null,
        ""P4"": {
            ""P1"": 1,
            ""P2"": null,
            ""P3"": null
        },
        ""FooArray"": [
            {
                ""F1"": null,
                ""F2"": null,
                ""F3"": null
            }
        ]
    },
    ""Bar"": null
}";

JToken token = JsonHelper.RemoveEmptyChildren(JToken.Parse(json));
Console.WriteLine(token.ToString(Formatting.Indented));

输出:

{
  "Foo": {
    "P2": "hello world",
    "P4": {
      "P1": 1
    },
    "FooArray": [
      {}
    ]
  }
}

Fiddle: https://dotnetfiddle.net/wzEOie

请注意,删除所有空值后,FooArray 中将有一个您可能不想要的空对象。 (如果该对象被删除,那么您将得到一个空的 FooArray,您也可能不想要它。)如果您想让辅助方法更积极地删除它,您可以将 IsEmpty 函数更改为这个:

    public static bool IsEmpty(JToken token)
    {
        return (token.Type == JTokenType.Null) ||
               (token.Type == JTokenType.Array && !token.HasValues) ||
               (token.Type == JTokenType.Object && !token.HasValues);
    }

有了这个更改,您的输出将如下所示:

{
  "Foo": {
    "P2": "hello world",
    "P4": {
      "P1": 1
    }
  }
}

Fiddle: https://dotnetfiddle.net/ZdYogJ

Brian 的回答有效。在发布问题后不久,我还想出了另一种(但仍然是递归的)方法,以防其他人感兴趣。

private void RemoveNullNodes(JToken root)
{
    if (root is JValue)
    {
        if (((JValue)root).Value == null)
        {
            ((JValue)root).Parent.Remove();
        }
    }
    else if (root is JArray)
    {
        ((JArray)root).ToList().ForEach(n => RemoveNullNodes(n));
        if (!(((JArray)root)).HasValues)
        {
            root.Parent.Remove();
        }
    }
    else if (root is JProperty)
    {
        RemoveNullNodes(((JProperty)root).Value);
    }
    else
    {
        var children = ((JObject)root).Properties().ToList();
        children.ForEach(n => RemoveNullNodes(n));

        if (!((JObject)root).HasValues)
        {
            if (((JObject)root).Parent is JArray)
            {
                ((JArray)root.Parent).Where(x => !x.HasValues).ToList().ForEach(n => n.Remove());
            }
            else
            {
                var propertyParent = ((JObject)root).Parent;
                while (!(propertyParent is JProperty))
                {
                    propertyParent = propertyParent.Parent;
                }
                propertyParent.Remove();
            }
        }
    }
}

您可以通过指定 JsonSerializer 并将其 NullValueHandler 设置为 NullValueHandler.Ignore 来防止创建空标记。这是作为参数传递给 JObject.FromObject 的,如您链接到的同一问题的答案所示:.

以下是我能够想到的。它删除仅包含空值的属性。这意味着它将处理 属性 是一个标量值为 null 的情况,并且还将处理存在一个全为 null 值的数组的情况。它还会删除没有值的属性。这处理了 属性 包含没有子属性的对象的情况。请注意,我使用的 JObject 具有 Descendents() 方法,这使得实现变得容易。 JToken 没有。我的实现改变了 JObject 本身,而不是创建它的副本。此外,它会继续删除属性,直到不再出现为止。它比其他实现更简洁。我不知道它在性能方面的比较如何。

using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Linq;

namespace JsonConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            var jo = JObject.Parse(File.ReadAllText(@"test.json"));
            Console.WriteLine($"BEFORE:\r\n{jo}");
            jo.RemoveNullAndEmptyProperties();
            Console.WriteLine($"AFTER:\r\n{jo}");
        }
    }

    public static class JObjectExtensions
    {
        public static JObject RemoveNullAndEmptyProperties(this JObject jObject)
        {
            while (jObject.Descendants().Any(jt => jt.Type == JTokenType.Property && (jt.Values().All(a => a.Type == JTokenType.Null) || !jt.Values().Any())))
                foreach (var jt in jObject.Descendants().Where(jt => jt.Type == JTokenType.Property && (jt.Values().All(a => a.Type == JTokenType.Null) || !jt.Values().Any())).ToArray())
                    jt.Remove();
            return jObject;
        }
    }
}

程序输出如下:

BEFORE:
{
  "propertyWithValue": "",
  "propertyWithObjectWithProperties": {
    "nestedPropertyWithValue": "",
    "nestedPropertyWithNull": null
  },
  "propertyWithEmptyObject": {},
  "propertyWithObjectWithPropertyWithNull": {
    "nestedPropertyWithNull": null
  },
  "propertyWithNull": null,
  "emptyArray": [],
  "arrayWithNulls": [
    null,
    null
  ],
  "arrayWithObjects": [
    {
      "propertyWithValue": ""
    },
    {
      "propertyWithNull": null
    }
  ]
}
AFTER:
{
  "propertyWithValue": "",
  "propertyWithObjectWithProperties": {
    "nestedPropertyWithValue": ""
  },
  "arrayWithObjects": [
    {
      "propertyWithValue": ""
    },
    {}
  ]
}