Filtern Sie alle Vorkommen bestimmter Eigenschaften aus dem JSON-Objekt heraus

Ich verwende PowerShell, um Daten aus einem API-Aufruf zu extrahieren, zu aktualisieren und dann an die API zurückzugeben.

Was ich gerne wissen würde, ist, ob es eine einfache Möglichkeit gibt, das JSON-Objekt zu ändern, um alle Eigenschaften herauszufiltern, die an keiner Stelle innerhalb der JSON-Struktur erwünscht sind?

Ich habe Folgendes versucht, aber im resultierenden JSON wurden nur die niedrigstes Level-Eigenschaften entfernt (z. B. "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

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

Ich möchte, dass alle $exlude-Einträge entfernt werden, unabhängig davon, wo sie sich innerhalb des JSON befinden. Gibt es eine einfache Lösung?

Aktualisieren

Hier ist ein weiteres (komplizierteres) JSON-Beispiel:

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

Das erwartete Ergebnis (alle p1- und p2-Schlüssel entfernt):

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

🤔 А знаете ли вы, что...
PowerShell поддерживает работу с REST API и веб-службами.


2
80
1

Antwort:

Gelöst

Leider scheint es keinen einfach-Weg zu geben. Es erwies sich tatsächlich als ziemlich herausfordernd, Arrays korrekt zu handhaben. Mein Ansatz besteht darin, das Eingabeobjekt (JSON) einschließlich aller Arrays rekursiv aufzurollen, damit wir problemlos Filter anwenden und dann ein neues Objekt aus den gefilterten Eigenschaften erstellen können.

Die Schritte eins und drei sind in die folgenden wiederverwendbaren Hilfsfunktionen verpackt, eine zum Entrollen (ConvertTo-FlatObjectValues) und eine zum Neuaufbau des Objekts (ConvertFrom-FlatObjectValues). Es gibt noch eine dritte Funktion (ConvertFrom-TreeHashTablesToArrays), die aber nur intern von ConvertFrom-FlatObjectValues verwendet wird.

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

Anwendungsbeispiel:

$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

Ausgabe:

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

Nutzungshinweise:

  • Untergeordnete Objekte werden entfernt, wenn sie nach dem Filtern keine Eigenschaften enthalten. Leere Arrays werden ebenfalls entfernt. Sie können dies verhindern, indem Sie -KeepEmptyObjects und/oder -KeepEmptyArrays an ConvertTo-FlatObjectValues übergeben.
  • Wenn der Eingabe-JSON ein Array auf Stammebene ist, stellen Sie sicher, dass Sie es als Argument an ConvertTo-FlatObjectValues übergeben, anstatt es zu leiten (was es entrollen würde und die Funktion nicht mehr wissen würde, dass es sich um ein Array handelt).
  • Es kann auch über den gesamten Pfad einer Eigenschaft gefiltert werden. Z.B. Um die P1-Eigenschaft nur innerhalb des a-Objekts zu entfernen, könnten Sie Where-Object Path -ne a.p1 schreiben. Um zu sehen, wie Pfade aussehen, rufen Sie einfach ConvertTo-FlatObjectValues $example auf, was die flache Liste von Eigenschaften und Array-Elementen ausgibt:
    Path      Name Value
    ----      ---- -----
    a.p1      p1   value 1
    a.c       c    value c
    a.d.e     e    value e
    a.d.p2    p2   value 3
    a.f.#0.g  g    value ga
    a.f.#0.p1 p1   value 4a
    a.f.#1.g  g    value gb
    a.f.#1.p1 p1   value 4b
    p2        p2   value 2
    b         b    value b
    

Hinweise zur Implementierung:

  • Während des Entrollens erstellt ConvertTo-FlatObjectValues separate Pfadsegmente (Schlüssel) für Array-Elemente, die wie „#n“ aussehen, wobei n der Array-Index ist. Dadurch können wir Arrays und Objekte einheitlicher behandeln, wenn wir das Objekt in ConvertFrom-FlatObjectValues neu erstellen.

  • ConvertFrom-FlatObjectValues erstellt zuerst verschachtelte Hashtabellen für alle Objekte und Arrays in seinem Abschnitt process. Dies macht es einfach, Eigenschaften in ihren jeweiligen Objekten zu speichern. In diesem Teil des Codes gibt es noch keine spezielle Behandlung von Arrays. Das Zwischenergebnis sieht nun so aus:

    {
      "a": {
        "c": "value c",
        "d": {
          "e": "value e"
        },
        "f": {
          "#0": {
            "g": "value ga"
          },
          "#1": {
            "g": "value gb"
          }
        }
      },
      "b": "value b"
    }
    
  • Nur im Abschnitt end von ConvertFrom-FlatObjectValues werden die Arrays aus den Hashtabellen neu erstellt, was durch die Funktion ConvertFrom-TreeHashTablesToArrays erledigt wird. Es wandelt Hashtabellen, deren Schlüssel mit „#“ beginnen, wieder in tatsächliche Arrays um. Aufgrund der Filterung könnten die Indizes nicht fortlaufend sein, sodass wir einfach die Werte sammeln und die Indizes ignorieren könnten. Obwohl für den gegebenen Anwendungsfall nicht erforderlich, werden die Array-Indizes sortiert, um die Funktion robuster zu machen und Indizes zu unterstützen, die in beliebiger Reihenfolge empfangen werden.

  • Die Rekursion in PowerShell-Funktionen ist aufgrund des Parameterbindungs-Overheads vergleichsweise langsam. Wenn Leistung im Vordergrund steht, sollte der Code in Inline-C# neu geschrieben werden oder Datenstrukturen wie Collections.Queue verwenden, um Rekursion zu vermeiden (auf Kosten der Lesbarkeit des Codes).