alphadev
alphadev

Reputation: 1659

PSCustomObject to Hashtable

What is the easiest way to convert a PSCustomObject to a Hashtable? It displays just like one with the splat operator, curly braces and what appear to be key value pairs. When I try to cast it to [Hashtable] it doesn't work. I also tried .toString() and the assigned variable says its a string but displays nothing - any ideas?

Upvotes: 98

Views: 122546

Answers (10)

iRon
iRon

Reputation: 23663

Probably a little over the top for this question, but I created a ObjectGraphTools module that includes a Copy-ObjectGraph cmdlet that is able to (recursively) convert any array like node to any other array like node (-ListAs <type or example>) and any map like node (incloding dictionaries and PSCustomObjexts) to any other map like node (-MapAs <type or example>).

Example command:

$Object | Copy-ObjectGraph -ListAs Collections.Generic.List[object] -MapAs @{}

Upvotes: 0

Dave Wyatt
Dave Wyatt

Reputation: 1568

Here's a version that works with nested hashtables / arrays as well, which is useful if you're trying to do this with DSC ConfigurationData:

function ConvertPSObjectToHashtable
{
    param (
        [Parameter(ValueFromPipeline)]
        $InputObject
    )

    process
    {
        if ($null -eq $InputObject) { return $null }

        if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string])
        {
            $collection = @(
                foreach ($object in $InputObject) { ConvertPSObjectToHashtable $object }
            )

            Write-Output -NoEnumerate $collection
        }
        elseif ($InputObject -is [psobject])
        {
            $hash = @{}

            foreach ($property in $InputObject.PSObject.Properties)
            {
                $hash[$property.Name] = (ConvertPSObjectToHashtable $property.Value).PSObject.BaseObject
            }

            $hash
        }
        else
        {
            $InputObject
        }
    }
}

Upvotes: 36

RiverHeart
RiverHeart

Reputation: 852

Hash table deep clone except we swap out PSObjects for hashtables when we find them. Otherwise it behaves the same as the function it's based on.

<#
.SYNOPSIS

Converts a PSObject to a hashtable by doing a deep clone
and converting PSObjects to Hashtables on the fly.

.NOTES

This function is based on Kevin Marquette's Get-DeepClone
function as documented below.
https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-hashtable?view=powershell-7.3#deep-copies

.EXAMPLE

$Settings = [PSObject]@{
    foo = "foo"
    one = @{ two = "three" }
    four = [PSObject]@{ five = "six" }
    seven = @( @("eight", "nine") )
}

$Clone = Convert-PSObjectToHashtable $Settings
#>
function Convert-PSObjectToHashtable {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [Object] $InputObject
    )

    process {
        $Clone = @{}
        switch ($InputObject.GetType().Name) {
            'PSCustomObject' {
                foreach ($Property in $InputObject.PSObject.Properties) {
                    $Clone[$Property.Name] = Convert-PSObjectToHashtable $Property.Value
                }
                return $Clone
            }
            'Hashtable' {
                foreach ($Key in $InputObject.Keys) {
                    $Clone[$Key] = Convert-PSObjectToHashtable $InputObject[$Key]
                }
                return $Clone
            }
            default { return $InputObject }
        }
    }
}

Upvotes: 0

Olli
Olli

Reputation: 375

Based on Dave's suggestion, here's a truly universal conversion function. It mimics CovertTo-Json | ConvertFrom-Json -AsHashTable deliberately only partly because this has some limitations:

  • Partially unforeseeable deviations in the results, depending on whether the object to be converted is passed as parameter or via pipeline.
  • Partially bloated results and bad scaling with the desired nesting depth when an object is piped: just try to convert a piped FileInfo object with Depth 6: it takes a long time and at the end I get an OutOfMemoryException (PowerShell 7.3, 16 GB installed memory).
  • Completely worthless conversion of some types: try converting an XmlDocument, Int128 or BigInt object. Of course, the latter two are not defined according to the JSON 'standard'. But it would at least be possible to convert them to type String or throw an exception.

Hidden properties of objects are not taken into account, since this behavior of ConvertTo-Json is subject to some criticism anyway. Further explanations can be found in the comment-based help of the function.

function ConvertTo-Hashtable {
    <#
        .DESCRIPTION
            This function converts arbitrary objects into HashTables of their properties. If one of the properties is an object, the procedure is the same, up to the
            specified nesting depth. If this is reached, the properties of the object in question are no longer read out, but its ToString() method is called.

            All properties of root objects whose name does not begin with 'PS' are taken into account.
            The following properties are taken into account for all other objects:
            - Properties of the PSMemberType 'Property' unrestricted
            - Properties of the PSMemberType 'NoteProperty' only if the property value's type is PSCustomObject
            - all other PSMemberTypes only if they have the PSMemberType of the preceding property in the hierarchy (affects ScriptProperty)
            Restrictively, properties marked as 'hidden' or referencing the parent object are ignored.

            Excluded from conversion to HashTables are all objects of types for which PowerShell allows remote delivery of 'live' objects. Objects of these types are kept
            unchanged. This affects the primitive and almost primitive types, as explained at
            https://devblogs.microsoft.com/powershell/how-objects-are-sent-to-and-from-remote-sessions/. Likewise, the composition of objects in arrays (collections that
            implement IEnumerable) or associative arrays (collections that implement IDictionary) is preserved. This circumvents the JSON 'standard' restrictions on
            convertible types imposed when using ConvertFrom-Json with the -AsHashTable switch.

            A conversion of a complex object into a HashTable can be advantageous if this is to be transmitted remotely. The nesting depth of these objects is now not
            subject to any restrictions. If, on the other hand, a complex object is transmitted remotely directly, PowerShell only transfers arrays and HashTables without
            restrictions regarding their nesting depth. However, objects of a different type contained therein are only transmitted in the form of the result of their
            ToString() method reaching a nesting depth of 2. All of their properties are lost. So an Object(Arrays(Object with different properties)) becomes a
            PSObject(Arrays(String)).

            If this function is used for remote transmission, it is only necessary to ensure that a mechanism is established on the receiving side to convert the HashTable
            structure back into a 'living' object of the relevant classes. The simplest possibility is to equip these with an op_Implicit operator that takes a
            HashTable. A suitable constructor of the class must then be called within the operator. This means that automated casting, e.g. of typed arrays, is also possible.
            These links may be helpful: https://stackoverflow.com/questions/58330233/how-to-convert-pscustomobject-with-additional-props-to-a-custom-class/ and
            https://stackoverflow.com/questions/59899360/how-do-i-pass-a-class-object-in-a-argument-list-to-a-another-computer-and-call-a/76695304#76695304.

        .PARAMETER InputObject
            The object to be convertet into a HashTable. There are no type restrictions.

        .PARAMETER Depth
            The level of nesting to which the properties of objects are considered. Beyond that, its ToString() method is called.

        .PARAMETER NoEnumerate
            Specifies that output isn't enumerated. The automatic enumeration and the function of this switch is documented in the following table. It also documents
            under which circumstances the nesting depth is reduced.

                passed value            return if passed via pipeline       return if passed as parameter
                    parameter isn't set (*automatic enumeration):
                        $null                     $null                               $null
                        item                      item, depth reduced by 1            item
                        []                        $null                            *  $null
                        [$null]                *  $null                            *  $null
                        [item]                 *  item, depth reduced by 1         *  item, depth reduced by 1
                        [items]                   [items]                             [items]

                    parameter is set (*changes due to the set parameter):
                        $null                  *  [$null]                             $null
                        item                   *  [item]                              item
                        []                     *  []                               *  []
                        [$null]                *  [$null]                          *  [$null]
                        [item]                 *  [item]                           *  [item]
                        [items]                   [items]                             [items]

        .PARAMETER EnumsAsStrings
            If this switch is set, Enums will be converted to their string representation. Otherwise their numeric value will be taken.

        .LINK
            https://stackoverflow.com/questions/3740128/pscustomobject-to-hashtable
    #>
    [CmdletBinding()]
    [SuppressMessage('PSUseOutputTypeCorrectly', '', Justification = 'Returns many types')]

    param (
        [Parameter(ValueFromPipeline)] [Object]$InputObject,
        [Parameter()] [ValidateRange(1, 100)] [Int32]$Depth = 2,
        [Parameter()] [Switch]$NoEnumerate,
        [Parameter()] [Switch]$EnumsAsStrings
    )

    begin {
        function InnerConvert(
            [Object]$InputObject,
            [Int32]$Depth,
            [Int32]$DepthCtr,
            [Boolean]$EnumsAsStrings,
            [Int32]$ObjectDepthStatus,
            [PSMemberTypes]$ParentMemberType
        ) {
            if ($null -eq $InputObject) {
                $null
            } elseif ($InputObject -is [Char] -or $InputObject -is [String] -or
                $InputObject -is [Byte] -or $InputObject -is [SByte] -or $InputObject -is [Int16] -or $InputObject -is [UInt16] -or
                $InputObject -is [Int32] -or $InputObject -is [UInt32] -or $InputObject -is [Int64] -or
                $InputObject -is [Single] -or $InputObject -is [Double] -or $InputObject -is [DateTime] -or $InputObject -is [Boolean] -or
                $InputObject -is [Decimal] -or $InputObject -is [Uri] -or $InputObject -is [Xml.XmlDocument] -or
                $InputObject -is [ProgressRecord] -or $InputObject -is [TimeSpan] -or $InputObject -is [Guid] -or
                $InputObject -is [Version] -or $InputObject -is [UInt64]
            ) {
                $InputObject
            } elseif ($InputObject -is [Enum]) {
                if ($EnumsAsStrings) {
                    $InputObject.ToString()
                } else {
                    [Int32]$InputObject
                }
            } elseif ($InputObject -is [SecureString] ) {
                $InputObject | ConvertFrom-SecureString
            } elseif ($InputObject -is [UInt64] -or $InputObject.GetType().Name -in ('Int128', 'UInt128') -or $InputObject -is [BigInt]) {
                [String]$InputObject
            } else {
                if ($DepthCtr -le $Depth) {
                    if ($InputObject -is [IDictionary]) {
                        try {
                            [OrderedHashTable]$resultTable = [ordered]@{}
                        } catch {
                            [HashTable]$resultTable = @{}
                        }
                        foreach ($item in $InputObject.GetEnumerator()) {
                            $resultTable[$item.Key] = InnerConvert -InputObject $item.Value -Depth $Depth -DepthCtr ($DepthCtr + 1) `
                                -EnumsAsStrings $EnumsAsStrings -ObjectDepthStatus $ObjectDepthStatus -ParentMemberType 0
                        }
                        $resultTable
                    } elseif ($InputObject -is [IEnumerable]) {
                        [Object[]]$resultArray = @(
                            foreach ($item in $InputObject) {
                                InnerConvert -InputObject $item -Depth $Depth -DepthCtr ($DepthCtr + 1) `
                                    -EnumsAsStrings $EnumsAsStrings -ObjectDepthStatus $ObjectDepthStatus -ParentMemberType 0
                            }
                        )
                        , $resultArray
                    } else {
                        # One must not test for [PSObject] because of some object properties are not of this type, e.g. RuntimeTypeHandle
                        try {
                            [OrderedHashTable]$resultTable = [ordered]@{}
                        } catch {
                            [HashTable]$resultTable = @{}
                        }
                        $ParentMemberType = $ParentMemberType -band -bnot [PSMemberTypes]::NoteProperty
                        [PSMemberTypes]$alwaysIncludedTypes = [PSMemberTypes]::Property -bor $ParentMemberType
                        foreach ($property in $InputObject.PSObject.Properties) {
                            if (-not $InputObject.Equals($property.Value)) {
                                if ($property.MemberType -band $alwaysIncludedTypes -or (
                                        $ObjectDepthStatus -lt 2 -and $property.Name -inotlike 'PS*'
                                    ) -or (
                                        $property.MemberType -band [PSMemberTypes]::NoteProperty -and (
                                            $null -eq $property.Value -or $property.Value.GetType().Name -in ('PSCustomObject', 'PSObject'))
                                    )
                                ) {
                                    $resultTable[$property.Name] = InnerConvert -InputObject $property.Value -Depth $Depth -DepthCtr ($DepthCtr + 1) `
                                        -EnumsAsStrings $EnumsAsStrings -ObjectDepthStatus 2 -ParentMemberType $property.MemberType
                                }
                            }
                        }
                        if ($resultTable.Count -or $InputObject -isnot [ValueType]) {
                            $resultTable
                        } else {
                            [String]$InputObject
                        }
                    }
                } else {
                    [String]$InputObject
                }
            }
        }
        if ($MyInvocation.ExpectingInput) {
            [Object]$completeInput = [ArrayList]::new()
        } else {
            [Object]$completeInput = $null
        }
    } process {
        if ($MyInvocation.ExpectingInput) {
            [void]$completeInput.Add($_)
        } else {
            $completeInput = $InputObject
        }
    } end {
        [Int32]$currentDepth = $Depth
        if ($MyInvocation.ExpectingInput -or $completeInput -is [Array]) {
            # don't enumerate HashTables and other IEnumerable
            if ($completeInput.Count -eq 0) {
                if (-not $NoEnumerate) {
                    $completeInput = $null
                }
            } elseif ($completeInput.Count -eq 1) {
                if ($NoEnumerate) {
                    if ($MyInvocation.ExpectingInput) {
                        $completeInput = $completeInput[0]
                    } else {
                        $currentDepth--
                    }
                } else {
                    $completeInput = $completeInput[0]
                    $currentDepth--
                }
            }
        }
        InnerConvert -InputObject $completeInput -Depth $currentDepth -DepthCtr 0 `
            -EnumsAsStrings $EnumsAsStrings -ObjectDepthStatus 0 -ParentMemberType 0
    }
}

Upvotes: 2

alainQtec
alainQtec

Reputation: 41

For simple [PSCustomObject] to [Hashtable] conversion Keith's Answer works best.

However if you need more options you can use


function ConvertTo-Hashtable {
    <#
    .Synopsis
        Converts an object to a hashtable
    .DESCRIPTION
        PowerShell v4 seems to have trouble casting some objects to Hashtable.
        This function is a workaround to convert PS Objects to [Hashtable]
    .LINK
        https://github.com/alainQtec/.files/blob/main/src/scripts/Converters/ConvertTo-Hashtable.ps1
    .NOTES
        Base ref: https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/turning-objects-into-hash-tables-2
    #>
    PARAM(
        # The object to convert to a hashtable
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        $InputObject,

        # Forces the values to be strings and converts them by running them through Out-String
        [switch]$AsString,

        # If set, empty properties are Included
        [switch]$AllowNulls,

        # Make each hashtable to have it's own set of properties, otherwise,
        # (default) each InputObject is normalized to the properties on the first object in the pipeline
        [switch]$DontNormalize
    )
    BEGIN {
        $headers = @()
    }
    PROCESS {
        if (!$headers -or $DontNormalize) {
            $headers = $InputObject | Get-Member -type Properties | Select-Object -expand name
        }
        $OutputHash = @{}
        if ($AsString) {
            foreach ($col in $headers) {
                if ($AllowNulls -or ($InputObject.$col -is [bool] -or ($InputObject.$col))) {
                    $OutputHash.$col = $InputObject.$col | Out-String -Width 9999 | ForEach-Object { $_.Trim() }
                }
            }
        } else {
            foreach ($col in $headers) {
                if ($AllowNulls -or ($InputObject.$col -is [bool] -or ($InputObject.$col))) {
                    $OutputHash.$col = $InputObject.$col
                }
            }
        }
    }
    END {
        return $OutputHash
    }
}

Maybe this is overkill but I hope it Helps

Upvotes: 0

Svyatoslav Pidgorny
Svyatoslav Pidgorny

Reputation: 643

My extremely lazy approach, enabled by a new feature in PowerShell 6:

$myhashtable = $mypscustomobject | ConvertTo-Json | ConvertFrom-Json -AsHashTable

Upvotes: 27

Hu Xinlong
Hu Xinlong

Reputation: 27

My code:

function PSCustomObjectConvertToHashtable() {
    param(
        [Parameter(ValueFromPipeline)]
        $object
    )

    if ( $object -eq $null ) { return $null }

    if ( $object -is [psobject] ) {
        $result = @{}
        $items = $object | Get-Member -MemberType NoteProperty
        foreach( $item in $items ) {
            $key = $item.Name
            $value = PSCustomObjectConvertToHashtable -object $object.$key
            $result.Add($key, $value)
        }
        return $result
    } elseif ($object -is [array]) {
        $result = [object[]]::new($object.Count)
        for ($i = 0; $i -lt $object.Count; $i++) {
            $result[$i] = (PSCustomObjectConvertToHashtable -object $object[$i])
        }
        return ,$result
    } else {
        return $object
    }
}

Upvotes: 1

Keith Hill
Keith Hill

Reputation: 201652

Shouldn't be too hard. Something like this should do the trick:

# Create a PSCustomObject (ironically using a hashtable)
$ht1 = @{ A = 'a'; B = 'b'; DateTime = Get-Date }
$theObject = new-object psobject -Property $ht1

# Convert the PSCustomObject back to a hashtable
$ht2 = @{}
$theObject.psobject.properties | Foreach { $ht2[$_.Name] = $_.Value }

Upvotes: 123

mhenry1384
mhenry1384

Reputation: 7678

This works for PSCustomObjects created by ConvertFrom_Json.

Function ConvertConvertFrom-JsonPSCustomObjectToHash($obj)
{
    $hash = @{}
     $obj | Get-Member -MemberType Properties | SELECT -exp "Name" | % {
                $hash[$_] = ($obj | SELECT -exp $_)
      }
      $hash
}

Disclaimer: I barely understand PowerShell so this is probably not as clean as it could be. But it works (for one level only).

Upvotes: 4

Shay Levy
Shay Levy

Reputation: 126732

Keith already gave you the answer, this is just another way of doing the same with a one-liner:

$psobject.psobject.properties | foreach -begin {$h=@{}} -process {$h."$($_.Name)" = $_.Value} -end {$h}

Upvotes: 39

Related Questions