【问题标题】:Iterate IDictionary<string, string> with dynamic nested JSON as value in C#使用动态嵌套 JSON 作为 C# 中的值迭代 IDictionary<string, string>
【发布时间】:2021-09-17 13:43:31
【问题描述】:

我的应用程序接收到包含 Dictionary&lt;string,string&gt; 作为属性之一的 Kafka 消息,它的值可能是一个嵌套的(但是是动态的)json 字符串,我需要遍历这个未知的 json。我正在努力寻找一个逻辑,甚至是最好的数据结构来进行这个迭代。

字典示例(模拟数据):

//could have complex nested json string as value
"reward":"{
  'xp':'200', 
  'gp':'150', 
  'loot':'{
    'item':'sword',
    'rarity': 'low'
  }'
}",
"achievement":"win_match"

// while other messages might be simple
"type":"generic_xp",
"percent":"100",
"status":"complete"

真实消息的序列化版本:

"{\"player_stats\":\"{\\\"assist\\\":0,\\\"deaths\\\":0,\\\"kills\\\":0,\\\"team_index\\\":2}\",\"round_attr\":\"{\\\"max_player_count\\\":4,\\\"rdur\\\":0,\\\"round\\\":1,\\\"team_player_count\\\":{\\\"team_1\\\":1,\\\"team_2\\\":0},\\\"team_score\\\":0}\",\"custom\":\"{\\\"armor\\\":\\\"armor_pickup_lv2\\\",\\\"balance\\\":550,\\\"helmet\\\":\\\"helmet_pickup_lv2\\\",\\\"misc\\\":[{\\\"count\\\":48,\\\"item_id\\\":\\\"shotgun\\\"},{\\\"count\\\":120,\\\"item_id\\\":\\\"bullet\\\"},{\\\"count\\\":2,\\\"item_id\\\":\\\"health_pickup_combo_small\\\"},{\\\"count\\\":2,\\\"item_id\\\":\\\"health_pickup_health_small\\\"}],\\\"weapon_1\\\":\\\"mp_weapon_semipistol\\\",\\\"weapon_2\\\":\\\"mp_weapon_shotgun_pistol\\\"}\",\"gdur\":\"0\"}"

更复杂的

  • 创建模型类不是一个选项,因为这个 json 是完全动态的
  • 无法对字典进行扁平化,因为 json 可能具有重复的键名,但在不同的层次结构下
  • 我无法请求更改 Kafka 消息

我要做什么

最终用户将定义我需要检查是否找到匹配项的规则。例如,规则可以是reward.xp == 200reward.loot.rarity == highstatus == complete。这些规则将由用户定义,因此不能硬编码,但是我可以决定使用数据结构来保存它们。因此,对于每条 Kafka 消息,我必须遍历该字典并尝试找到与规则匹配的内容。

我的尝试

我尝试过JsonConvert.DeserializeobjectdynamicExpandoObject,但没有一个可以处理嵌套的 json 层次结构。他们刚刚答对了第一级。与JObject.Parse 的结果相同。

【问题讨论】:

  • 如果 json 字符串是动态的,为什么要坚持反序列化为具体类型?只持有 jsonobject 并获取您想要的任何数据怎么样?既然你标记了.net core,你会使用内置的json库吗?
  • json.net 应该使用 JObject.Parse 处理这个问题。要进行迭代,您需要对树中的节点进行递归访问。如果需要,您可以将其展平为列表 - 使用节点的完整路径作为列表中的键(例如 reward.loot.rarity)。
  • “重复的键名,但在不同的层次结构下”是什么意思?你能举个例子吗?
  • @BrianRogers 例如reward 可以在根目录中,如上例所示,但也可能在achievement.reward 下。所以如果我把结构弄平,我会得到重复键的错误。
  • @andrew JObject.Parse 只正确解析了第一级。在奖励示例中,我正确地得到了孩子xpgp,但是对于loot,它没有孩子,而是保留了字符串值。所以在这个例子中,loot 属性变成了一个字符串属性,它的值是一个类似于序列化示例的转义字符串

标签: c# .net-core json.net dynamic-programming


【解决方案1】:

使用您喜欢的任何解析器解析 JSON(我使用了 Newtonsoft.Json)。

然后递归地访问层次结构并将每个属性复制到一个平面列表中,使用每个属性值的完整路径作为键。然后,您可以迭代该平面列表。

编辑:评论请求支持数组,所以这个版本支持。

https://dotnetfiddle.net/6ykHT0

using System;
using Newtonsoft.Json.Linq;
using System.Linq;
using System.Collections.Generic;
                    
public class Program
{
    public static void Main()
    {
        string json = @"{
                        'reward': { 
                            'xp': '200', 
                            'gp': '150', 
                            'loot': {
                                'item': 'sword',
                                'rarity': 'low',
                                'blah': {
                                    'socks': 5
                                }
                            },
                            'arrayofint': [1,2,3,4],
                            'arrayofobj': [
                                {
                                    'foo': 'bar',
                                    'stuff': ['omg!', 'what?!']
                                },
                                {
                                    'foo': 'baz',
                                    'stuff': ['a', 'b']
                                }
                            ],
                            'arrayofarray': [
                                [1,2,3],
                                [4,5,6]
                            ],
                            'arrayofheterogenousjunk': [
                                'a',
                                2,
                                { 'objprop': 1 },
                                ['staahp!']
                            ]
                        },
                        'achievement': 'win_match'
                    }";
        
        JObject data = JObject.Parse(json);
        IList<string> nodes = flattenJSON(data);
        
        Console.WriteLine(string.Join(Environment.NewLine, nodes));
    }
    
    private static IList<string> flattenJSON(JToken token)
    {
        return _flattenJSON(token, new List<string>());
    }

    private static IList<string> _flattenJSON(JToken token, List<string> path)
    {
        var output = new List<string>();
        if (token.Type == JTokenType.Object)
        {
            // Output the object's child properties
            output.AddRange(token.Children().SelectMany(x => _flattenJSON(x, path)));
        }
        else if (token.Type == JTokenType.Array)
        {
            // Output each array element
            var arrayIndex = 0;
            foreach (var child in token.Children())
            {
                // Append the array index to the end of the last path segment - e.g. someProperty[n]
                var newPath = new List<string>(path);
                newPath[newPath.Count - 1] += "[" + arrayIndex++ + "]";
                output.AddRange(_flattenJSON(child, newPath));
            }
        }
        else if (token.Type == JTokenType.Property)
        {
            var prop = token as JProperty;
            // Insert the property name into the path
            output.AddRange(_flattenJSON(prop.Value, new List<string>(path) { prop.Name }));
        }
        else
        {
            // Join the path segments delimited with periods, followed by the literal value
            output.Add(string.Join(".", path) + " = " + token.ToString());
        }
        return output;
    }
}

输出:

reward.xp = 200
reward.gp = 150
reward.loot.item = sword
reward.loot.rarity = low
reward.loot.blah.socks = 5
reward.arrayofint[0] = 1
reward.arrayofint[1] = 2
reward.arrayofint[2] = 3
reward.arrayofint[3] = 4
reward.arrayofobj[0].foo = bar
reward.arrayofobj[0].stuff[0] = omg!
reward.arrayofobj[0].stuff[1] = what?!
reward.arrayofobj[1].foo = baz
reward.arrayofobj[1].stuff[0] = a
reward.arrayofobj[1].stuff[1] = b
reward.arrayofarray[0][0] = 1
reward.arrayofarray[0][1] = 2
reward.arrayofarray[0][2] = 3
reward.arrayofarray[1][0] = 4
reward.arrayofarray[1][1] = 5
reward.arrayofarray[1][2] = 6
reward.arrayofheterogenousjunk[0] = a
reward.arrayofheterogenousjunk[1] = 2
reward.arrayofheterogenousjunk[2].objprop = 1
reward.arrayofheterogenousjunk[3][0] = staahp!
achievement = win_match

以前的版本(不支持阵列)

这不正确地支持数组 - 它会将作为原始 JSON 的数组属性的内容输出 - 即它不会遍历数组。

https://dotnetfiddle.net/yZbwul

public static void Main()
{
    string json = @"{
                    'reward': { 
                        'xp': '200', 
                        'gp': '150', 
                        'loot': {
                            'item': 'sword',
                            'rarity': 'low',
                            'blah': {
                                'socks': 5
                            }
                        }
                    },
                    'achievement': 'win_match'
                }";
    
    JObject data = JObject.Parse(json);
    IList<string> nodes = flattenJSON(data, new List<string>());
    
    Console.WriteLine(string.Join(Environment.NewLine, nodes));
}

private static IList<string> flattenJSON(JObject obj, IList<string> path)
{
    var output = new List<string>();
    foreach (var prop in obj.Properties())
    {
        if (prop.Value.Type == JTokenType.Object)
        {
            output.AddRange(flattenJSON(prop.Value as JObject, new List<string>(path){prop.Name}));
        }
        else
        {
            var s = string.Join(".", new List<string>(path) { prop.Name }) + " = " + prop.Value.ToString();
            output.Add(s);
        }
    }
    return output;
}

输出:

reward.xp = 200
reward.gp = 150
reward.loot.item = sword
reward.loot.rarity = low
reward.loot.blah.socks = 5
achievement = win_match

【讨论】:

  • 您还可以添加一些列表(值)示例吗?
  • @LeiYang - 你的意思是一个值为数组的属性吗?
  • 是的。你已经很好地解决了嵌套键,剩下的一个关键特征是数组值。
  • @TiagoBrenck - 我不熟悉图书馆。也许会问一个单独的问题,关于为什么子对象用引号括起来以及如何在 C# 中反序列化。我希望有一种现有的方法可以简单地做到这一点,而不必手动处理(在序列化或反序列化时)。
  • @andrew 我找到了无效 JSON 的修复程序。 Kafka 消息上的字典会将嵌套的 Json 包装在一个字符串中,因此内置转换器认为该字符串只是一个值,而不是一个新的 Json。因此,为了让您的出色解决方案能够处理这种烦人的消息格式,我不得不添加这个else if(token.Type == JTokenType.String &amp;&amp; token.ToString().StartsWith("{")) { var jObj = JObject.Parse(token.ToString()); output.AddRange(jObj.Children().SelectMany(x =&gt; _FlattenJSON(x, path))); }
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-11
  • 2018-08-23
相关资源
最近更新 更多