Skip to main content
PowerShell · lesson 5 of 12
PowerShell intermediate 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

powershell files csv json export-csv

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.

Frequently Asked Questions

Why did a Tags property turn into the literal text 'System.Object[]' in the CSV?
CSV is a flat, text-only format — every value has to become a single string, and Export-Csv does that by calling .ToString() on anything that isn't already a simple type. An array's .ToString() is its .NET type name, not its contents, so the actual tag values are silently lost rather than raising any kind of error.
Why did numbers imported from CSV sort incorrectly?
CSV has no type information at all — every column comes back from Import-Csv as a plain string, even one that displays as 1965. Sort-Object then compares those strings character by character, the same trap covered with $numbers back in the pipeline lesson. Cast the property to [int] before sorting to fix it.
Does JSON have the same flattening problem as CSV?
No — JSON has real arrays and nested objects as part of the format, so ConvertTo-Json followed by ConvertFrom-Json round-trips a Tags array (or any nested structure) intact. CSV's advantage is that it opens directly in a spreadsheet; JSON's is that it actually preserves structure.
What does -NoTypeInformation on Export-Csv do?
Without it, older Windows PowerShell versions wrote a #TYPE line above the header naming the .NET type being exported, which most other tools reading the CSV don't expect and choke on. PowerShell 7 no longer writes that line by default, but the parameter still exists — passing it explicitly costs nothing and keeps a script's behavior consistent across PowerShell versions.
Can Get-ChildItem filter by more than a file extension?
Yes — its output is real file objects with properties like LastWriteTime, Length, and Extension, so Get-ChildItem | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } finds files untouched in a month using an actual date comparison, not a text pattern. -Filter is faster for a simple extension match; Where-Object is what handles everything -Filter can't express.