使用基于特定条件的关联键选择嵌套对象的优雅方法

时间:2018-02-20 14:55:26

标签: json jq

给出JSON中的示例文档,类似于:

{
  "id": "post-1",
  "type": "blog-post",
  "tags": [
    {
      "id": "tag-1",
      "name": "Tag 1"
    },
    {
      "id": "tag-2",
      "name": "Tag 2"
    }
  ],
  "heading": "Post 1",
  "body": "this is my first blog post",
  "links": [
    {
      "id": "post-2",
      "heading": "Post 2",
      "tags": [
        {
          "id": "tag-1",
          "name": "Tag 1"
        },
        {
          "id": "tag-3",
          "name": "Tag 3"
        }
      ]
    }
  ],
  "metadata": {
    "user": {
      "social": [
        {
          "id": "twitter",
          "handle": "@user"
        },
        {
          "id": "facebook",
          "handle": "123456"
        },
        {
          "id": "youtube",
          "handle": "ABC123xyz"
        }
      ]
    },
    "categories": [
      {
        "name": "Category 1"
      },
      {
        "name": "Category 2"
      }
    ]
  }
}

我想选择具有属性"id"的任何对象(无论深度),以及父对象的属性名称。上面的例子应该只是一个例子。我不能自由分享的实际数据可以有任何深度和任何结构。可以随时引入和删除属性。使用Blog Post样式只是因为它非常受欢迎,我的想象力非常有限。

该属性表示域中的特定类型,也可能(但不一定)编码为属性的值。

如果一个对象没有"id"属性,那么它就没有意义,不应该被选中。

一个非常重要的特殊情况是当属性的值是一个对象数组时,在这种情况下,我需要保留属性名称并将其与数组中的每个元素相关联。

所需输出的一个例子是:

[
  {
    "type": "tags",
    "node": {
      "id": "tag-1",
      "name": "Tag 1"
    }
  },
  {
    "type": "tags",
    "node": {
      "id": "tag-2",
      "name": "Tag 2"
    }
  },
  {
    "type": "links",
    "node": {
      "id": "post-2",
      "heading": "Post 2",
      "tags": [
        {
          "id": "tag-1",
          "name": "Tag 1"
        },
        {
          "id": "tag-3",
          "name": "Tag 3"
        }
      ]
    }
  },
  {
    "type": "tags",
    "node": {
      "id": "tag-1",
      "name": "Tag 1"
    }
  },
  {
    "type": "tags",
    "node": {
      "id": "tag-3",
      "name": "Tag 3"
    }
  },
  {
    "type": "social",
    "node": {
      "id": "twitter",
      "handle": "@user"
    }
  },
  {
    "type": "social",
    "node": {
      "id": "facebook",
      "handle": "123456"
    }
  },
  {
    "type": "social",
    "node": {
      "id": "youtube",
      "handle": "ABC123xyz"
    }
  }
]

输出是完全相同的,输出是完全相同的,例如,与我的用例无关,它也可以被分组。由于顶级对象具有属性"id",因此可以包含特殊名称,但如果根本不包含该名称,我会更喜欢。

我尝试使用walkreducerecurse无济于事,我担心我的jq技能太有限了。但我认为一个好的解决方案至少会使用其中一个。

我想要一个类似

的表达式
to_entries[] | .value | .. | select(has("id")?)

会选择正确的对象,但..我不再能够保留关联的属性名称。

我提出的最好的是

. as $document
| [paths | if length > 1 and .[-1] == "id" then .[0:-1] else empty end] 
| map(. as $path 
      | $document 
      | { "type": [$path[] | if type == "string" then . else empty end][-1],
           "node": getpath($path) })

哪个有效,但感觉非常复杂并且涉及首先提取所有路径,忽略任何没有"id"作为最后一个元素的路径,然后删除"id"段以获取实际路径对象并存储(现在是最后一个)作为字符串的段,该段对应于包含感兴趣对象的父对象属性。最后,通过getpath选择实际对象。

是否有更优雅或最短的表达方式?

我应该注意,我希望使用jq以方便绑定到其他语言以及能够在命令行上运行程序。

对于这个问题的范围,我并不真正对jq的替代品感兴趣,因为我可以想象如何使用其他工具以不同的方式解决这个问题,但我真的很想"只是& #34;使用jq

2 个答案:

答案 0 :(得分:1)

这有效:

[
    foreach (paths | select(.[-1] == "id" and length > 1)[:-1]) as $path ({i:.};
        .o = {
            type: last($path[] | strings),
            node: (.i | getpath($path))
        };
        .o
    )
]

诀窍是要知道路径中的任何数字都表示该值是数组的一部分。您必须调整路径才能获取父名称。但是使用带有字符串过滤器的last/1会使其更简单。

答案 1 :(得分:1)

由于实际要求对我来说并不清楚,我将假设给定的实现定义了功能要求,并提出了一个更短,更有希望的更光滑的版本:

. as $document
| paths
| select(length > 1 and .[-1] == "id")
| .[0:-1] as $path
| { "type": last($path[] | strings),
    "node": $document | getpath($path) }

这会产生一个流,所以如果你想要一个数组,你可以简单地将上面的方括号括起来。

如果流为空,则

last(stream)会发出null,这符合.[-1]的行为。