我正在使用PowerShell从API调用中提取数据,进行更新,然后将其传递回API.

我想知道的是,是否有一种简单的方法来修改JSON对象,过滤掉JSON struct 中任何位置不需要的所有properties个对象?

我try 了以下方法,但是生成的JSON只删除了lowest level个属性(即"p2")

$example = ConvertFrom-Json '{"a":{"p1": "value1"},"p2": "value2", "b":"valueb"}'
$exclude = "p1", "p2"
$clean = $example | Select-Object -Property * -ExcludeProperty $exclude
ConvertTo-Json $clean -Compress

结果=>{"a":{"p1":"value1"},"b":"valueb"}

我希望删除所有$exlude个条目,无论它们位于JSON中的哪个位置.有简单的解决办法吗?

Update

下面是另一个(更复杂的)JSON示例:

{
  "a": {
    "p1": "value 1",
    "c": "value c",
    "d": {
      "e": "value e",
      "p2": "value 3"
    },
    "f": [
      {
      "g": "value ga",
      "p1": "value 4a"
      },
      {
      "g": "value gb",
      "p1": "value 4b"
      }
    ]
  },
  "p2": "value 2",
  "b": "value b"
}

预期结果(删除所有p1和p2键):

{
  "a": {
    "c": "value c",
    "d": {
      "e": "value e"
    },
    "f": [
      {
        "g": "value ga"
      },
      {
        "g": "value gb"
      }
    ]
  },
  "b": "value b"
}

推荐答案

不幸的是,似乎没有easy条路.事实证明,正确处理数组非常具有挑战性.我的方法是递归地展开输入(JSON)对象,包括任何数组,这样我们就可以轻松地应用过滤,然后根据过滤后的属性构建一个新对象.

步骤1和3包含在以下可重用的辅助函数中,一个用于展开(ConvertTo-FlatObjectValues),另一个用于重建对象(ConvertFrom-FlatObjectValues).还有第三个函数(ConvertFrom-TreeHashTablesToArrays),但它只在ConvertFrom-FlatObjectValues内部使用.

Function ConvertTo-FlatObjectValues {
    <#
    .SYNOPSIS
        Unrolls a nested PSObject/PSCustomObject "property bag".
    .DESCRIPTION
        Unrolls a nested PSObject/PSCustomObject "property bag" such as created by ConvertFrom-Json into flat objects consisting of path, name and value.
        Fully supports arrays at the root as well as for properties and nested arrays.
    #>
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)] $InputObject,
        [string] $Separator = '.',
        [switch] $KeepEmptyObjects,
        [switch] $KeepEmptyArrays,
        [string] $Path,    # Internal parameter for recursion.
        [string] $Name     # Internal parameter for recursion.
    )
    
    process {
        if( $InputObject -is [System.Collections.IList] ) {

            if( $KeepEmptyArrays ) {
                # Output a special item to keep empty array.
                [PSCustomObject]@{ 
                    Path  = ($Path, "#").Where{ $_ } -join $Separator
                    Name  = $Name
                    Value = $null
                }
            }

            $i = 0
            $InputObject.ForEach{
                # Recursively unroll array elements.
                $childPath = ($Path, "#$i").Where{ $_ } -join $Separator
                ConvertTo-FlatObjectValues -InputObject $_ -Path $childPath -Name $Name `
                                           -Separator $Separator -KeepEmptyObjects:$KeepEmptyObjects -KeepEmptyArrays:$KeepEmptyArrays
                $i++
            }
        }
        elseif( $InputObject -is [PSObject] ) {

            if( $KeepEmptyObjects ) {
                # Output a special item to keep empty object.
                [PSCustomObject]@{ 
                    Path  = $Path
                    Name  = $Name
                    Value = [ordered] @{}
                }
            }

            $InputObject.PSObject.Properties.ForEach{
                # Recursively unroll object properties.
                $childPath = ($Path, $_.Name).Where{ $_ } -join $Separator
                ConvertTo-FlatObjectValues -InputObject $_.Value -Path $childPath -Name $_.Name `
                                           -Separator $Separator -KeepEmptyObjects:$KeepEmptyObjects -KeepEmptyArrays:$KeepEmptyArrays
            }
        }
        else {
            # Output scalar

            [PSCustomObject]@{ 
                Path  = $Path
                Name  = $Name
                Value = $InputObject 
            }
        }
    }
}

function ConvertFrom-FlatObjectValues {
    <#
    .SYNOPSIS
        Convert a flat list consisting of path and value into tree(s) of PSCustomObject.
    .DESCRIPTION
        Convert a flat list consisting of path and value, such as generated by ConvertTo-FlatObjectValues, into tree(s) of PSCustomObject.
        The output can either be an array (not unrolled) or a PSCustomObject, depending on the structure of the input data.
    #>
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $Path,
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [AllowNull()] $Value,
        [Parameter()] [string] $Separator = '.'
    )

    begin {
        $tree = [ordered]@{}
    }

    process {
        # At first store everything (including array elements) into hashtables. 

        $branch = $Tree

        do {
            # Split path into root key and path remainder.
            $key, $path = $path.Split( $Separator, 2 )

            if( $path ) {
                # We have multiple path components, so we may have to create nested hash table.
                if( -not $branch.Contains( $key ) ) {
                    $branch[ $key ] = [ordered] @{}
                }           
                # Enter sub tree. 
                $branch = $branch[ $key ]
            }
            else {
                # We have arrived at the leaf -> set its value
                $branch[ $key ] = $value
            }
        }
        while( $path )
    }

    end {
        # So far we have stored the original arrays as hashtables with keys like '#0', '#1', ... (possibly non-consecutive).
        # Now convert these hashtables back into actual arrays and generate PSCustomObject's from the remaining hashtables.
        ConvertFrom-TreeHashTablesToArrays $tree
    }
}

Function ConvertFrom-TreeHashTablesToArrays {
    <#
    .SYNOPSIS
        Internal function called by ConvertFrom-FlatObjectValues.
    .DESCRIPTION
        - Converts arrays stored as hashtables into actual arrays.
        - Converts any remaining hashtables into PSCustomObject's. 
    #>
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)] [Collections.IDictionary] $InputObject
    )

    process {    
        # Check if $InputObject has been generated from an array.
        $isArray = foreach( $key in $InputObject.Keys ) { $key.StartsWith('#'); break }

        if( $isArray ) {
            # Sort array indices as they might be unordered. A single '#' as key will be skipped, because it denotes an empty array.
            $sortedByKeyNumeric = $InputObject.GetEnumerator().Where{ $_.Key -ne '#' } | 
                                   Sort-Object { [int]::Parse( $_.Key.SubString( 1 ) ) }

            $outArray = $sortedByKeyNumeric.ForEach{
                
                if( $_.Value -is [Collections.IDictionary] ) {
                    # Recursion. Output array element will either be an object or a nested array.
                    ConvertFrom-TreeHashTablesToArrays $_.Value
                }
                else {
                    # Output array element is a scalar value.
                    $_.Value
                }
            }

            , $outArray  # Comma-operator prevents unrolling of the array, to support nested arrays.
        }
        else {
            # $InputObject has been generated from an object. Copy it to $outProps recursively and output as PSCustomObject.

            $outProps = [ordered] @{}

            $InputObject.GetEnumerator().ForEach{

                $outProps[ $_.Key ] = if( $_.Value -is [Collections.IDictionary] ) {
                    # Recursion. Output property will either be an object or an array.
                    ConvertFrom-TreeHashTablesToArrays $_.Value
                }
                else {
                    # Output property is a scalar value.
                    $_.Value
                }
            }

            [PSCustomObject] $outProps
        }
    }
}

Usage example:

$example = ConvertFrom-Json @'
{
  "a": {
    "p1": "value 1",
    "c": "value c",
    "d": {
      "e": "value e",
      "p2": "value 3"
    },
    "f": [
      {
      "g": "value ga",
      "p1": "value 4a"
      },
      {
      "g": "value gb",
      "p1": "value 4b"
      }
    ]
  },
  "p2": "value 2",
  "b": "value b"
}
'@

$exclude = "p1", "p2"

$clean = ConvertTo-FlatObjectValues $example |  # Step 1: unroll properties 
         Where-Object Name -notin $exclude |    # Step 2: filter
         ConvertFrom-FlatObjectValues           # Step 3: rebuild object

$clean | ConvertTo-Json -Depth 9

Output:

{
  "a": {
    "c": "value c",
    "d": {
      "e": "value e"
    },
    "f": [
      {
        "g": "value ga"
      },
      {
        "g": "value gb"
      }
    ]
  },
  "b": "value b"
}

Usage Notes:

  • 如果过滤后子对象不包含任何属性,则会将其删除.空数组也会被删除.您可以通过将-KeepEmptyObjects和/或-KeepEmptyArrays传递给函数ConvertTo-FlatObjectValues来防止这种情况.
  • 如果输入JSON是根级别的数组,请确保将其作为参数传递给ConvertTo-FlatObjectValues,而不是管道传输(这样会展开它,函数将不再知道它是数组).
  • 过滤也可以在属性的整个路径上进行.例如,要删除a对象中的P1属性,可以写Where-Object Path -ne a.p1.要查看路径的外观,只需调用ConvertTo-FlatObjectValues $example,它将输出属性和数组元素的平面列表:

Implementation Notes:

  • 在展开过程中,ConvertTo-FlatObjectValues为看起来像"#n"的数组元素创建单独的路径段(键),其中n是数组索引.在ConvertFrom-FlatObjectValues中重建对象时,这使我们能够更统一地处理数组和对象.

  • ConvertFrom-FlatObjectValues首先为其process部分中的所有对象和数组创建嵌套哈希表.这样可以很容易地将属性重新收集到各自的对象中.在这部分代码中,仍然没有对数组进行特殊处理.中间结果如下所示:

    {
      "a": {
        "c": "value c",
        "d": {
          "e": "value e"
        },
        "f": {
          "#0": {
            "g": "value ga"
          },
          "#1": {
            "g": "value gb"
          }
        }
      },
      "b": "value b"
    }
    
  • 只有在ConvertFrom-FlatObjectValuesend部分中,数组是从哈希表重建的,这是由函数ConvertFrom-TreeHashTablesToArrays完成的.它将键以"#"开头的哈希表转换回实际array.由于过滤,索引可能是非连续的,因此我们可以只收集值而忽略索引.虽然对于给定的用例不是必需的,但数组索引将被排序,以使函数更健壮,并支持以任何顺序接收的索引.

  • 由于参数绑定开销,PowerShell函数中的递归相对较慢.如果性能至关重要,那么代码应该用内联C#重写,或者使用像Collections.Queue这样的数据 struct 来避免递归(以牺牲代码可读性为代价).

Json相关问答推荐

JOLT将对象名作为新属性添加到主体中

如何在使用GO时检测JSON中不需要的字段?

无法访问id的第三级json

使用 JOLT 将日期格式转换为 JSON

Jolt Spec 跳过数组中的第一个元素

当列为空时从 SQL 获取 JSON

使用不同行数的数据创建嵌套Jolt

将JSON行转换为TSV格式, for each 数组项生成单独的行

将 JSON 字符串解析为 Kotlin Android 中的对象列表(MOSHI?)

我无法将来自 API 的数据显示到 FlatList 中,但我在 console.log 中看到了它.问题是什么?

将哈希表转换为 json 后,Powershell 缺少数组

如何使用 ConfigurationBuilder 解析现有的 json 字符串(不是文件)

N1QL 聚合查询 Couchbase

如何让 JSON.NET 忽略对象关系?

Jackson 中的 readValue 和 readTree:何时使用哪个?

将文件发送到 Rails JSON API

我可以使用空字符串作为对象标识符吗?

未调用 npm package.json 脚本

Protocol Buffer vs Json - 何时 Select 一个而不是另一个

你如何在 Arrays of Arrays 上 OPENJSON