90

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?

chazbot7
  • 598
  • 3
  • 12
  • 34
alphadev
  • 1,529
  • 5
  • 18
  • 20
  • 1
    PSCustomObjects have advantages over hashtables. Think twice before converting it. http://stackoverflow.com/questions/22002748/hashtables-from-convertfrom-json-have-different-type-from-powershells-built-in-h/22010290#22010290 – spuder Dec 09 '16 at 17:38
  • 3
    Splatting doesn't work with a PSCustomObject, is a good reason I can think of. – Brain2000 Jan 11 '19 at 01:32

10 Answers10

118

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 }
jpaugh
  • 6,634
  • 4
  • 38
  • 90
Keith Hill
  • 194,368
  • 42
  • 353
  • 369
  • 1
    Note that `$_.Name` is already a string, so `$ht2[$_.Name]` or `$h.($_.Name)` will work just as well as `"$($_.Name)"`. – Emperor XLII Jun 05 '11 at 14:53
  • 19
    Note that this doesn't work for PSCustomObjects created by `ConvertFrom-Json`. [This question](http://stackoverflow.com/q/22002748/310446) addresses that issue. – BenV Jan 16 '15 at 23:22
  • 5
    @BenV: Just to clarify: The problem stems from _nested_ custom objects, not from use of `ConvertFrom-Json` per se, which also produces `[PSCustomObject]` instances. In other words: a JSON source that produces non-nested objects works just fine; e.g.: `('{ "foo": "bar" }' | ConvertFrom-Json).psobject.properties | % { $ht = @{} } { $ht[$_.Name] = $_.Value } { $ht }` – mklement0 Jan 20 '16 at 19:59
  • 2
    Casting could become a reality in the future: https://connect.microsoft.com/PowerShell/feedback/details/679841/allow-casting-of-psobject-or-object-to-hashtable – W1M0R Mar 08 '16 at 11:22
  • See below an answer from @Svyatoslav Pidgorny which is using new features in PowerShell 6 or 7 for more simple approach! https://stackoverflow.com/a/61742479/3425553 – Igor May 27 '20 at 10:24
37

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}
Shay Levy
  • 121,444
  • 32
  • 184
  • 206
  • Heh, started with something very similar except that it was just long enough to invoke the SO horizontal scrollbar. BTW I think your `$'s` are missing some `_'s`. :-) – Keith Hill Sep 18 '10 at 16:13
  • That's what I was trying to avoid and eventually it swallowed the underscore sign. Thanks! – Shay Levy Sep 19 '10 at 15:56
  • @ShayLevy: What is the advantage of putting everything on the same line? – MonkeyDreamzzz May 11 '15 at 13:30
  • 3
    Nice; if you use `%` and positional parameters as the blocks, you can shorten to `$psobject.psobject.properties | % { $ht = @{} } { $ht[$_.Name] = $_.Value } { $ht }`. @Rubanov: It doesn't have to be on a single _line_, but the advantage is that a single _statement_ (pipeline) creates the hashtable. – mklement0 Jan 20 '16 at 19:41
34

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] = (Convert-PSObjectToHashtable $property.Value).PSObject.BaseObject
            }

            $hash
        }
        else
        {
            $InputObject
        }
    }
}
mwallner
  • 985
  • 11
  • 23
Dave Wyatt
  • 1,508
  • 14
  • 9
  • 3
    This is the only version that worked for my data with multi-level nested objects and arrays. – Jeffrey Harmon Feb 09 '16 at 12:55
  • 4
    Excellent & elegant solution for the multi-level nested objects. – Petru Zaharia Sep 21 '16 at 00:33
  • As noted in a previous answer's comment, this code above handles complex / nested hashtables and is great for handling content from `ConvertFrom-Json`. See also [this question](http://stackoverflow.com/q/22002748/310446) – Thomas BDX May 09 '20 at 12:27
  • I am unable to get this to work "as-is" for nested objects: `@{ Name = "test1"; nested = @{ license = 'x'; cert = 'y' } } | Convert-PSObjectToHashTable` Instead, I had to add a `GetEnumerator()` on Line 15: `foreach ($object in $InputObject.GetEnumerator()) { ConvertPSObjectToHashtable $object }` – Keith S Garner Jul 13 '20 at 04:15
  • The same code by Adam Bertram can be found here: https://4sysops.com/archives/convert-json-to-a-powershell-hash-table/ – Ciove Aug 18 '20 at 10:32
  • This worked for me, but not if the $InputObject already was, or contained, a HashTable or Ordered Dictionary. I added the following just after `if ($null -eq $InputObject...` - add this line: `if ($InputObject -is [Hashtable] -or $InputObject.GetType().Name -eq 'OrderedDictionary') { return $InputObject }` – PSaul Jan 27 '22 at 22:48
24

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

$myhashtable = $mypscustomobject | ConvertTo-Json | ConvertFrom-Json -AsHashTable
  • It's worth bearing in mind that convertto-json has a limit on the depth it will traverse your object tree. The default depth is 2 which for something complex may be insufficient. The maximum depth can be changed by specifying -depth but even this is limited to at maximum 100. A warning message is omitted if the max depth is exceeded but it generates an incomplete object representation and would not halt your script unless you've customized your warning preference. – hannasm Feb 09 '23 at 17:04
4

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).

mhenry1384
  • 7,538
  • 5
  • 55
  • 74
  • 1
    Little more cleaner (may be tougher to understand) `$hash=@{};$obj | Get-Member -MemberType Properties | foreach { $hash.Add($_.Name,$obj.($_.Name))}` – Adarsha May 17 '17 at 02:38
1

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
    }
}
Hu Xinlong
  • 27
  • 4
0

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

alainQtec
  • 41
  • 5
0

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
    }
}
Olli
  • 327
  • 2
  • 9
0

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 }
        }
    }
}
RiverHeart
  • 549
  • 6
  • 15
-1

Today, the "easiest way" to convert PSCustomObject to Hashtable would be so:

$custom_obj | ConvertTo-HashtableFromPsCustomObject    

OR

[hashtable]$custom_obj

Conversely, you can convert a Hashtable to PSCustomObject using:

[PSCustomObject]$hash_table

Only snag is, these nifty options may not be available in older versions of PS

Ifedi Okonkwo
  • 3,406
  • 4
  • 33
  • 45