PowerShell Hashtable vs PSCustomObject: When to Use Each

PowerShell Hashtable vs PSCustomObject: When to Use Each

PowerShell Tips Editor 3 min read
PowerShell Hashtable vs PSCustomObject: When to Use Each

Both hashtables and PSCustomObjects store named data in PowerShell, and both look similar when you create them. But they behave completely differently in the pipeline, have different property access rules, and perform differently at scale. Choosing between them correctly prevents subtle bugs — like a hashtable that exports wrong data to CSV or a PSCustomObject that is slower to look up a key by name. This guide explains the real differences between PowerShell hashtable vs PSCustomObject so you choose the right tool every time.

Hashtable Creation and Access

Create a hashtable with @{} and access values by key using either dot notation or bracket notation. Key order is not guaranteed unless you use [ordered]:

# Standard hashtable — key order not guaranteed
$server = @{
    Name      = "Server01"
    IPAddress = "10.1.1.50"
    Status    = "Online"
    MemoryGB  = 16
}

# Access values
$server.Name          # "Server01"
$server['IPAddress']  # "10.1.1.50"

# Add a key
$server.OS = "Windows Server 2022"

# Remove a key
$server.Remove('MemoryGB')

# Ordered hashtable — preserves insertion order
$ordered = [ordered]@{ First = 1; Second = 2; Third = 3 }
$ordered.Keys   # First, Second, Third (in order)

PSCustomObject Creation

Create a PSCustomObject with the [PSCustomObject]@{} cast. Properties are always ordered in insertion order (no need for [ordered]). PSCustomObjects are designed to work in the pipeline:

$server = [PSCustomObject]@{
    Name      = "Server01"
    IPAddress = "10.1.1.50"
    Status    = "Online"
    MemoryGB  = 16
}

# Access values (same dot notation)
$server.Name         # "Server01"
$server.IPAddress    # "10.1.1.50"

# Add a property
$server | Add-Member -NotePropertyName OS -NotePropertyValue "Windows Server 2022"

# Check type
$server.GetType().Name   # PSCustomObject

Pipeline and Export Behavior

This is the biggest practical difference. PSCustomObjects flow through the pipeline as objects with named properties. Hashtables do not serialize cleanly to Export-Csv or display as expected in Format-Table:

$servers = @(
    [PSCustomObject]@{ Name = "Server01"; Status = "Online" }
    [PSCustomObject]@{ Name = "Server02"; Status = "Offline" }
)

# PSCustomObject — works perfectly
$servers | Export-Csv "C:\Reports\servers.csv" -NoTypeInformation
$servers | Format-Table -AutoSize

# Hashtable array — DOES NOT work as expected with Export-Csv
$htArray = @(
    @{ Name = "Server01"; Status = "Online" }
    @{ Name = "Server02"; Status = "Offline" }
)
$htArray | Export-Csv "C:\Reports\wrong.csv" -NoTypeInformation
# Exports the hashtable's own properties (Count, Keys, Values) — not your data!
Name      Status
----      ------
Server01  Online
Server02  Offline

Property Access Syntax Differences

Hashtable keys and PSCustomObject properties both support dot notation, but they differ in dynamic key access and how they handle missing keys:

$ht  = @{ Name = "Test"; Value = 42 }
$pso = [PSCustomObject]@{ Name = "Test"; Value = 42 }

# Dynamic key access — hashtable only supports this with brackets
$key = "Name"
$ht[$key]     # "Test" — works
$ht.$key      # "Test" — also works in PowerShell
$pso.$key     # "Test" — works

# Checking for key/property existence
$ht.ContainsKey("Name")                         # True (hashtable method)
$pso.PSObject.Properties["Name"] -ne $null      # True (PSCustomObject check)
[bool]($pso.PSObject.Properties.Name -eq "Name") # True

Performance for Large Collections

For key lookup by name in a large collection, hashtables are faster because they use hash-based indexing. PSCustomObjects enumerate properties for lookup. For small objects (under a few thousand items), the difference is negligible:

# Hashtable is faster for key lookup in large lookup tables
$lookup = @{}
1..10000 | ForEach-Object { $lookup["Key$_"] = "Value$_" }

# O(1) lookup
$value = $lookup["Key5000"]

# PSCustomObject is better for pipeline processing and reporting
$records = 1..10000 | ForEach-Object {
    [PSCustomObject]@{ Id = $_; Name = "Item$_" }
}
$records | Where-Object Id -gt 9990 | Format-Table

Conversion Between the Two

Convert a hashtable to a PSCustomObject when you need pipeline-friendly behavior, and back to a hashtable for key-lookup performance:

# Hashtable → PSCustomObject
$ht  = @{ Name = "Server01"; Status = "Online" }
$pso = [PSCustomObject]$ht   # Direct cast works

# PSCustomObject → Hashtable
$pso = [PSCustomObject]@{ Name = "Server01"; Status = "Online" }
$ht  = @{}
$pso.PSObject.Properties | ForEach-Object { $ht[$_.Name] = $_.Value }
$ht   # @{Name="Server01"; Status="Online"}

# Or in PS 7+ use the ConvertTo-Json | ConvertFrom-Json round-trip:
$ht2 = $pso | ConvertTo-Json | ConvertFrom-Json -AsHashtable

Common Errors and Fixes

  • Hashtable does not auto-serialize correctly in Export-Csv. When you pipe an array of hashtables to Export-Csv, you get a CSV of the hashtable’s internal properties (Count, IsReadOnly, Keys) rather than your data keys. Always convert to PSCustomObject first when exporting: $htArray | ForEach-Object { [PSCustomObject]$_ } | Export-Csv.
  • Property order in hashtable not guaranteed without [ordered]. Standard @{} hashtables do not preserve insertion order. When property order matters (CSV column order, output display), use [ordered]@{} or use a PSCustomObject, which always preserves property insertion order.

Related Cmdlets / See Also

Wrapping Up

Use PSCustomObjects when you need pipeline compatibility, Format-Table display, or Export-Csv export. Use hashtables when you need fast key lookup, splatting, or mapping configurations. Convert between them easily with a cast ([PSCustomObject]$ht) when your needs change mid-pipeline.

Send-Item -To