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.


