PowerShell · lesson 5 of 12
Files, CSV, and JSON: What Survives the Round Trip
Get-ChildItem returns real file objects to filter directly — but exporting one with a nested property to CSV silently flattens it to the text 'System.Object[]', and everything CSV imports comes back as a string.
By SkillByExample · Published
Get-ChildItem returns the same kind of live object every other cmdlet in this track has —
which makes what happens when that object gets flattened to CSV worth looking at closely.
Listing and filtering files
$demo = "$env:TEMP\ps-demo"
New-Item -ItemType Directory -Path $demo -Force | Out-Null
"a" | Out-File "$demo\report.csv"
"b" | Out-File "$demo\notes.txt"
"c" | Out-File "$demo\data.json"
"d" | Out-File "$demo\summary.csv"
Get-ChildItem $demo -Filter "*.csv" | Select-Object Name, Length
Name Length
---- ------
report.csv 3
summary.csv 3
-Filter matches on the file system provider’s own filtering, before PowerShell even builds
the objects — faster than piping everything through Where-Object for a simple extension
match like this one.
Export-Csv and Import-Csv
$data = @(
[pscustomobject]@{ Name = "Dune"; Year = 1965 }
[pscustomobject]@{ Name = "Neuromancer"; Year = 1984 }
)
$data | Export-Csv -Path "$env:TEMP\books.csv" -NoTypeInformation
Import-Csv "$env:TEMP\books.csv"
Name Year
---- ----
Dune 1965
Neuromancer 1984
Looks like a clean round trip. It isn’t quite one — the next two sections show what didn’t survive.
The bug: a nested property flattens to its type name
$data = @(
[pscustomobject]@{ Name = "Dune"; Tags = @("scifi","classic") }
[pscustomobject]@{ Name = "Foundation"; Tags = @("scifi","series") }
)
$data | Export-Csv -Path "$env:TEMP\books.csv" -NoTypeInformation
Get-Content "$env:TEMP\books.csv"
"Name","Tags"
"Dune","System.Object[]"
"Foundation","System.Object[]"
The actual tags are gone. CSV can only hold flat text, so Export-Csv calls .ToString() on
anything that isn’t already a simple value — an array’s .ToString() is its .NET type name,
System.Object[], not its contents. No error, no warning; the data is just quietly wrong.
The fix: flatten it yourself, on purpose
$data = @(
[pscustomobject]@{ Name = "Dune"; Tags = ("scifi","classic" -join ";") }
[pscustomobject]@{ Name = "Foundation"; Tags = ("scifi","series" -join ";") }
)
$data | Export-Csv -Path "$env:TEMP\books.csv" -NoTypeInformation
Get-Content "$env:TEMP\books.csv"
"Name","Tags"
"Dune","scifi;classic"
"Foundation","scifi;series"
-join ";" turns the array into one deliberate string before Export-Csv ever sees it — the
same information, chosen to survive the format instead of losing it to a default conversion.
The other CSV trap: everything comes back as a string
$data = @(
[pscustomobject]@{ Name = "Book A"; Copies = 9 }
[pscustomobject]@{ Name = "Book B"; Copies = 10 }
[pscustomobject]@{ Name = "Book C"; Copies = 2 }
)
$data | Export-Csv -Path "$env:TEMP\copies.csv" -NoTypeInformation
$imported = Import-Csv "$env:TEMP\copies.csv"
$imported | Sort-Object Copies
Name Copies
---- ------
Book B 10
Book C 2
Book A 9
10 sorted before 2. CSV carries no type information at all, so Copies comes back from
Import-Csv as a plain string on every row, even though it started as a real int — the
exact trap from the pipeline lesson’s $numbers example, just arriving by way of a file this
time. Cast it back before sorting:
$imported | Sort-Object { [int]$_.Copies }
Name Copies
---- ------
Book C 2
Book A 9
Book B 10
JSON: the format that doesn’t flatten
$data = @(
[pscustomobject]@{ Name = "Dune"; Tags = @("scifi","classic") }
)
$json = $data | ConvertTo-Json
$json
{
"Name": "Dune",
"Tags": [
"scifi",
"classic"
]
}
$back = $json | ConvertFrom-Json
$back.Tags
scifi
classic
The Tags array survives intact — JSON has real arrays and nested objects as part of the
format, so there’s nothing to flatten. CSV’s advantage is that it opens directly in a
spreadsheet; JSON’s is that ConvertFrom-Json hands back the actual shape of the data, not a
text approximation of it.
Practice
1. Export an object with a nested hashtable property to CSV and see what it becomes.
$data = [pscustomobject]@{ Name = "Dune"; Meta = @{ Pages = 412 } }
$data | Export-Csv -Path "$env:TEMP\meta.csv" -NoTypeInformation
Get-Content "$env:TEMP\meta.csv"
"Name","Meta"
"Dune","System.Collections.Hashtable"
Same failure as the array, different type name — any non-scalar property flattens to
whatever its .ToString() happens to produce, which for most .NET objects is just the type
name.
2. Convert that same nested object to JSON instead, and confirm the structure survives.
$data = [pscustomobject]@{ Name = "Dune"; Meta = @{ Pages = 412 } }
$data | ConvertTo-Json
{
"Name": "Dune",
"Meta": {
"Pages": 412
}
}
Meta stays a real nested object in the JSON — this is the same distinction as the Tags
array, just one level deeper.
3. Filter files by last-write time instead of extension, using a real date comparison.
Get-ChildItem $demo | Where-Object { $_.LastWriteTime -gt (Get-Date).AddMinutes(-5) } | Select-Object Name
Name
----
data.json
notes.txt
report.csv
summary.csv
LastWriteTime is a real [datetime] on the file object, so -gt (Get-Date).AddMinutes(-5)
is an actual time comparison — every file here shows up because the whole $demo folder was
just created in this lesson.
Next: the registry as a filesystem provider — a genuinely Windows-only PowerShell feature.