Creating Custom Objects
Note
Learn how to build your own PowerShell objects from scratch using PSCustomObject, Add-Member, and other techniques for structured data.
Overview
PowerShell lets you create your own objects — structured containers with named properties and values — rather than relying only on objects returned by cmdlets. Custom objects are essential for organizing data, building reports, and passing structured results through the pipeline.
PSCustomObject (Recommended)
[PSCustomObject] with a hashtable is the modern, preferred syntax for creating custom objects. It's fast, readable, and produces a fully-formed object immediately.
Basic Syntax
$person = [PSCustomObject]@{
Name = "Alice"
Age = 30
Department = "IT"
}
$person.Name # Alice
$person.Age # 30
$person.Department # IT
Why PSCustomObject vs. Just a Hashtable?
# Hashtable — properties are unordered, no dot notation by default
$hash = @{ Name = "Alice"; Age = 30 }
$hash.Name # Works, but...
$hash | Get-Member # Shows Hashtable, not custom object type
# PSCustomObject — looks and behaves like a real object
$obj = [PSCustomObject]@{ Name = "Alice"; Age = 30 }
$obj | Get-Member # Shows NoteProperty members
$obj | Format-Table # Renders as a table with column headers
PSCustomObject works seamlessly with Format-Table, Export-Csv, ConvertTo-Json, and the pipeline.
Ordered Properties
Wrap your hashtable in [ordered] to preserve the property order as declared:
$server = [PSCustomObject][ordered]@{
Hostname = "WEB-01"
IPAddress = "192.168.1.10"
OS = "Windows Server 2022"
Status = "Online"
}
$server | Format-Table # Columns appear in declared order
Always use [ordered] for reports
Without [ordered], hashtable keys may appear in unpredictable order in your output. Use [ordered] whenever column order matters.
Building Collections of Custom Objects
Custom objects shine when you build lists (arrays) for pipeline use.
Building with ForEach
$computers = "PC-01", "PC-02", "PC-03"
$results = $computers | ForEach-Object {
[PSCustomObject]@{
Computer = $_
Online = Test-Connection -ComputerName $_ -Count 1 -Quiet
CheckTime = Get-Date -Format "yyyy-MM-dd HH:mm"
}
}
$results | Format-Table -AutoSize
Building with a Loop
$report = @() # Empty array to collect results
foreach ($user in Get-LocalUser) {
$report += [PSCustomObject]@{
Name = $user.Name
Enabled = $user.Enabled
LastLogon = $user.LastLogon
}
}
$report | Sort-Object Name | Format-Table -AutoSize
Use [System.Collections.Generic.List] for large collections
Adding to an array with += inside a loop is slow for large sets because it copies the array every iteration. For better performance:
Adding Properties Dynamically with Add-Member
Add-Member lets you attach new properties or methods to an existing object after it's been created — including objects returned by cmdlets.
NoteProperty (Simple Value)
$obj = [PSCustomObject]@{ Name = "Alice" }
# Add a new property
$obj | Add-Member -MemberType NoteProperty -Name "Role" -Value "Admin"
$obj.Role # Admin
Add-Member in a Pipeline
# Enrich objects coming from a cmdlet
Get-Process | Select-Object -First 5 Name, CPU |
ForEach-Object {
$_ | Add-Member -MemberType NoteProperty -Name "Flagged" -Value ($_.CPU -gt 10) -PassThru
}
-PassThru returns the modified object so it continues down the pipeline.
ScriptProperty (Calculated Property)
A ScriptProperty computes its value from a script block each time it's accessed:
$file = Get-Item "C:\logs\app.log"
$file | Add-Member -MemberType ScriptProperty -Name "SizeMB" -Value {
[math]::Round($this.Length / 1MB, 2)
}
$file.SizeMB # Returns calculated value
Note
Inside ScriptProperty blocks, use $this to refer to the object itself.
ScriptMethod (Custom Action)
$server = [PSCustomObject]@{
Hostname = "WEB-01"
IP = "192.168.1.10"
}
$server | Add-Member -MemberType ScriptMethod -Name "Ping" -Value {
Test-Connection -ComputerName $this.IP -Count 1 -Quiet
}
$server.Ping() # Returns True or False
New-Object (Legacy Syntax)
New-Object PSObject is the older way to create custom objects. You'll see it in older scripts — it produces the same result as [PSCustomObject].
# Old way
$obj = New-Object PSObject -Property @{
Name = "Alice"
Age = 30
}
# Modern equivalent
$obj = [PSCustomObject]@{
Name = "Alice"
Age = 30
}
Prefer [PSCustomObject] in new code — it's faster and more concise.
New-Object for .NET Types
New-Object is still useful for creating instances of .NET classes:
# Create a .NET object (not a PSCustomObject)
$timer = New-Object System.Timers.Timer
$client = New-Object System.Net.WebClient
$sb = New-Object System.Text.StringBuilder
$sb.Append("Hello")
$sb.Append(", World")
$sb.ToString() # Hello, World
Practical Examples
Example: System Inventory Report
Scenario: Collect basic system info into a structured object
function Get-SystemInfo {
[PSCustomObject]@{
Hostname = $env:COMPUTERNAME
OSVersion = (Get-CimInstance Win32_OperatingSystem).Caption
TotalRAM_GB = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)
CPUName = (Get-CimInstance Win32_Processor).Name
Uptime = (Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
}
}
Get-SystemInfo | Format-List
Example: Disk Usage Summary
Scenario: Build a report of drive usage across all drives
$diskReport = Get-PSDrive -PSProvider FileSystem | ForEach-Object {
[PSCustomObject]@{
Drive = $_.Name
UsedGB = [math]::Round($_.Used / 1GB, 2)
FreeGB = [math]::Round($_.Free / 1GB, 2)
TotalGB = [math]::Round(($_.Used + $_.Free) / 1GB, 2)
UsedPct = if (($_.Used + $_.Free) -gt 0) {
"{0:P0}" -f ($_.Used / ($_.Used + $_.Free))
} else { "N/A" }
}
}
$diskReport | Format-Table -AutoSize
Example: CSV Import with Enrichment
Scenario: Import a CSV, then add a calculated field to each row
$data = Import-Csv "employees.csv"
$enriched = $data | ForEach-Object {
$obj = [PSCustomObject]@{
Name = $_.Name
Department = $_.Department
Salary = [decimal]$_.Salary
Bonus = [math]::Round([decimal]$_.Salary * 0.10, 2)
TotalComp = [math]::Round([decimal]$_.Salary * 1.10, 2)
}
$obj
}
$enriched | Export-Csv "employees-with-bonus.csv" -NoTypeInformation
Example: Function Returning a Custom Object
Scenario: Write a function that returns structured data instead of plain text
function Test-ServiceHealth {
param([string[]]$ServiceNames)
foreach ($name in $ServiceNames) {
$svc = Get-Service -Name $name -ErrorAction SilentlyContinue
[PSCustomObject]@{
Service = $name
Status = if ($svc) { $svc.Status } else { "Not Found" }
StartType = if ($svc) { $svc.StartType } else { "N/A" }
Healthy = ($svc -and $svc.Status -eq "Running")
}
}
}
Test-ServiceHealth -ServiceNames "wuauserv", "spooler", "FakeService" |
Format-Table -AutoSize
Exporting Custom Objects
Custom objects work directly with export cmdlets:
$data = Get-Process | Select-Object -First 10 | ForEach-Object {
[PSCustomObject]@{
Name = $_.Name
CPU = [math]::Round($_.CPU, 2)
MemoryMB = [math]::Round($_.WS / 1MB, 2)
}
}
# Export to CSV
$data | Export-Csv "processes.csv" -NoTypeInformation
# Export to JSON
$data | ConvertTo-Json | Out-File "processes.json"
# Display as table
$data | Format-Table -AutoSize
# Display as list
$data | Format-List
Related Topics
- Object Manipulation - Filtering, sorting, and grouping objects
- The Pipeline - Passing objects between commands
- Formatting Output - Displaying object data
- Working with JSON - Serializing objects to JSON