3

Some hashtables in PowerShell, such as those imported with Import-PowerShellDataFile, would be much easier to navigate if being a PSCustomObject instead.

@{
    AllNodes = @(
        @{
            NodeName = 'SRV1'
            Role = 'Application'
            RunCentralAdmin = $true
        },
        @{
            NodeName = 'SRV2'
            Role = 'DistributedCache'
            RunCentralAdmin = $true
        },
        @{
            NodeName = 'SRV3'
            Role = 'WebFrontEnd'
            PSDscAllowDomainUser = $true
            PSDscAllowPlainTextPassword = $true
            CertificateFolder = '\\mediasrv\Media'
        },
        @{
            NodeName = 'SRV4'
            Role = 'Search'
        },
        @{
            NodeName = '*'
            DatabaseServer = 'sql1'
            FarmConfigDatabaseName = '__FarmConfig'
            FarmContentDatabaseName = '__FarmContent'
            CentralAdministrationPort = 1234
            RunCentralAdmin = $false
        }
    );
    NonNodeData = @{
        Comment = 'No comment'
    }
}

When imported it will become a hashtable of hashtables

$psdnode = Import-PowerShellDataFile .\nodefile.psd1

$psdnode

Name                           Value
----                           -----
AllNodes                       {System.Collections.Hashtable, System.Collect...
NonNodeData                    {Comment}

$psdnode.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Hashtable                                System.Object

and the data structure will be just weird when navigating by property name.

Dennis
  • 871
  • 9
  • 29
  • 1
    "the data structure will be just weird when navigating by property name." - can you tell us more about what you mean? Dictionaries (including hashtables) are generally _easier_ to iterate/traverse than objects with dynamic member sets. – Mathias R. Jessen Sep 29 '22 at 11:40
  • Okay, I'm asking about _your preference_, care to share? What is it that makes it so horrible to work with? What is it that you can't do with a hashtable that you could do with a psobject? :) – Mathias R. Jessen Sep 29 '22 at 12:25
  • @MathiasR.Jessen That depends on your preference :) – Dennis Sep 29 '22 at 12:25
  • @MathiasR.Jessen For navigating the data structure, the code will be much smaller (eq. cleaner) to read. – Dennis Sep 29 '22 at 12:27
  • 1
    Are you sure? PowerShell has a dictionary adapter that exposes the keys as properties, so you can interrogate it the exact same way as a custom object: `foreach($node in $psdnode.AllNodes){ Write-Host $node.NodeName }` :) – Mathias R. Jessen Sep 29 '22 at 12:30
  • Yes, I'm aware of the possibilities. But I'd rather not iterate through an object if I already now what property to use... – Dennis Sep 29 '22 at 12:39
  • The only thing I'm iterating over here is the array contained in the `AllNodes` entry of the root dictionary. `$psdnode.AllNodes` <- exact same syntax as if `$psdnode` was a custom object. `$node.NodeName` <- exact same syntax as if `$node` was a custom object. – Mathias R. Jessen Sep 29 '22 at 12:41
  • Yes, but I don't need to iterate as all now :) – Dennis Sep 29 '22 at 13:56
  • 2
    So what is it that you'd want to do once the hashtable(s) have been turned into custom objects? What code would you write? – Mathias R. Jessen Sep 29 '22 at 13:58
  • The thing I prefer to do is to filter instead of iterating as in $nodes.AllNodes | Where NodeName -eq SRV1 – Dennis Sep 29 '22 at 15:23
  • 1
    And that's what I'm trying to tell you: you can already do that with hashtables :) `$psdnode.AllNodes |Where-Object NodeName -eq SRV1` _already works_ with the data you've presented – Mathias R. Jessen Sep 29 '22 at 15:49
  • Ah, sweet :) Then it's perhaps just the pivoted view that I prefer when outputting the data. – Dennis Sep 29 '22 at 17:44
  • yes if only Format-Table / Format-List would work with a hashtable.. – Brett Dec 01 '22 at 12:34

3 Answers3

9

There's good information in the existing answers, but given your question's generic title, let me try a systematic overview:

  • You do not need to convert a hashtable to a [pscustomobject] instance in order to use dot notation to drill down into its entries (properties), as discussed in the comments and demonstrated in iRon's answer.

    • A simple example:

      @{ top = @{ nested = 'foo' } }.top.nested  # -> 'foo'
      
    • See this answer for more information.

  • In fact, when possible, use of hashtables is preferable to [pscustomobject]s, because:

    • they are lighter-weight than [pscustomobject] instances (use less memory)
    • it is easier to construct them iteratively and modify them.

Note:


In cases where you do need to convert a [hasthable] to a [pscustomobject]:

While many standard cmdlets accept [hasthable]s interchangeably with [pscustomobjects]s, some do not, notably ConvertTo-Csv and Export-Csv (see GitHub issue #10999 for a feature request to change that); in such cases, conversion to [pscustomobject] is a must.

Caveat: Hasthables can have keys of any type, whereas conversion to [pscustomobject] invariably requires using string "keys", i.e. property names. Thus, not all hashtables can be faithfully or meaningfully converted to [pscustomobject]s.

  • Converting non-nested hashtables to [pscustomobject]:

    • The syntactic sugar PowerShell offers for [pscustomobject] literals (e.g., [pscustomobject] @{ foo = 'bar'; baz = 42 }) also works via preexisting hash; e.g.:

      $hash = @{ foo = 'bar'; baz = 42 } 
      $custObj = [pscustomobject] $hash   # Simply cast to [pscustomobject]
      
  • Converting nested hashtables, i.e. an object graph, to a [pscustomobject] graph:

    • A simple, though limited and potentially expensive solution is the one shown in your own answer: Convert the hashtable to JSON with ConvertTo-Json, then reconvert the resulting JSON into a [pscustomobject] graph with ConvertFrom-Json.

      • Performance aside, the fundamental limitation of this approach is that type fidelity may be lost, given that JSON supports only a few data types. While not a concern with a hashtable read via Import-PowerShellDataFile, a given hashtable may contain instances of types that have no meaningful representation in JSON.
    • You can overcome this limitation with a custom conversion function, ConvertFrom-HashTable (source code below); e.g. (inspect the result with Format-Custom -InputObject $custObj):

      $hash = @{ foo = 'bar'; baz = @{ quux = 42 } } # nested hashtable
      $custObj = $hash | ConvertFrom-HashTable # convert to [pscustomobject] graph
      

ConvertFrom-HashTable source code:

Note: Despite the name, the function generally supports instance of types that implement IDictionary as input.

function ConvertFrom-HashTable {
  param(
    [Parameter(Mandatory, ValueFromPipeline)]
    [System.Collections.IDictionary] $HashTable
  )
  process {
    $oht = [ordered] @{} # Aux. ordered hashtable for collecting property values.
    foreach ($entry in $HashTable.GetEnumerator()) {
      if ($entry.Value -is [System.Collections.IDictionary]) { # Nested dictionary? Recurse.
        $oht[$entry.Key] = ConvertFrom-HashTable -HashTable $entry.Value
      } else { # Copy value as-is.
        $oht[$entry.Key] = $entry.Value
      }
    }
    [pscustomobject] $oht # Convert to [pscustomobject] and output.
  }
}
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Wow, excellent points and explanations :) – Dennis Sep 29 '22 at 17:52
  • 1
    You know, I found this page trying to find a source to use as reference to support my own discovery of the Hashtable > Json > Json > PSCustomObject trick (I've referred to as the Json woop-dee-doo trick). – Jeremy Bradshaw Aug 30 '23 at 02:05
3

What is the issue/question?

@'
@{
    AllNodes = @(
        @{
            NodeName = 'SRV1'
            Role = 'Application'
            RunCentralAdmin = $true
        },
        @{
            NodeName = 'SRV2'
            Role = 'DistributedCache'
            RunCentralAdmin = $true
        },
        @{
            NodeName = 'SRV3'
            Role = 'WebFrontEnd'
            PSDscAllowDomainUser = $true
            PSDscAllowPlainTextPassword = $true
            CertificateFolder = '\\mediasrv\Media'
        },
        @{
            NodeName = 'SRV4'
            Role = 'Search'
        },
        @{
            NodeName = '*'
            DatabaseServer = 'sql1'
            FarmConfigDatabaseName = '__FarmConfig'
            FarmContentDatabaseName = '__FarmContent'
            CentralAdministrationPort = 1234
            RunCentralAdmin = $false
        }
    );
    NonNodeData = @{
        Comment = 'No comment'
    }
}
'@ |Set-Content .\nodes.psd1
$psdnode = Import-PowerShellDataFile .\nodefile.psd1

$psdnode

Name                           Value
----                           -----
NonNodeData                    {Comment}
AllNodes                       {SRV1, SRV2, SRV3, SRV4…}
$psdnode.AllNodes.where{ $_.NodeName -eq 'SRV3' }.Role
WebFrontEnd
iRon
  • 20,463
  • 10
  • 53
  • 79
1

A very simple way, that I discovered just yesterday, is to do a "double-convert" over JSON.

$nodes = Import-PowerShellDataFile .\nodes.psd1 | ConvertTo-Json | ConvertFrom-Json

$nodes

AllNodes
--------
{@{NodeName=SRV1; RunCentralAdmin=True; Role=Application}, @{NodeName=SRV2; RunCentralAdm...}

$nodes.GetType()   

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    PSCustomObject                           System.Object
Dennis
  • 871
  • 9
  • 29