PowerShell ConvertFrom-Json: Deep Dive with Real API Examples
─□✕

PowerShell ConvertFrom-Json: Deep Dive with Real API Examples

PowerShell Tips Editor 5 min read
PowerShell ConvertFrom-Json: Deep Dive with Real API Examples

ConvertFrom-Json looks simple until you face deeply nested structures, duplicate keys, or a 10 MB API payload. This powershell convertfrom-json deep dive covers every parameter and real-world edge case: the difference between PSCustomObject and hashtable output, recursive navigation helpers, streaming large files without loading everything into memory, and how PowerShell 5.1 silently truncates nested objects at depth 100.

Basic Conversion and Navigating PSCustomObject Properties

By default, ConvertFrom-Json returns a PSCustomObject. Property names become object members you access with dot notation, which feels natural for static schemas.

$json = '{"name":"Alice","age":30,"address":{"city":"Seattle","zip":"98101"}}'

$obj = $json | ConvertFrom-Json

# Dot notation navigation
Write-Host $obj.name             # Alice
Write-Host $obj.address.city     # Seattle

# Enumerate all top-level properties
$obj.PSObject.Properties | Select-Object Name, Value | Format-Table -AutoSize

# Check if a property exists before accessing
if ($obj.PSObject.Properties['email']) {
    Write-Host "Email: $($obj.email)"
} else {
    Write-Host "No email field in this response."
}

PSCustomObject property access is case-insensitive in PowerShell, so $obj.Name and $obj.name both work. This is different from .NET dictionaries and can mask bugs when you later switch to -AsHashtable.

Using -AsHashtable for Dynamic Property Access

When property names are unknown at write time — common with API responses where the key is an identifier — PSCustomObject becomes awkward. The -AsHashtable switch (PowerShell 6+) returns a proper [hashtable] you can iterate and index dynamically.

$json = '{"server-01":{"cpu":42,"mem":78},"server-02":{"cpu":15,"mem":55}}'

# PSCustomObject — dynamic property access is clunky
$obj = $json | ConvertFrom-Json
$obj.PSObject.Properties | ForEach-Object { Write-Host "$($_.Name): cpu=$($_.Value.cpu)" }

# Hashtable — clean iteration and indexing
$ht = $json | ConvertFrom-Json -AsHashtable
foreach ($key in $ht.Keys) {
    Write-Host "$key — cpu: $($ht[$key]['cpu'])  mem: $($ht[$key]['mem'])"
}

# Dynamic key lookup (impossible cleanly with PSCustomObject)
$target = 'server-01'
Write-Host "CPU for $target: $($ht[$target]['cpu'])"

Important: -AsHashtable is not available in Windows PowerShell 5.1. Check $PSVersionTable.PSVersion.Major and branch accordingly if you need to support both runtimes.

Handling Deeply Nested JSON with Recursive Helper Functions

In PowerShell 5.1, ConvertFrom-Json silently truncates objects nested beyond depth 100. In PowerShell 7+ the -Depth parameter defaults to 1024 but still has a maximum. Write a recursive helper to flatten or walk nested structures safely.

function ConvertTo-FlatHashtable {
    param(
        [Parameter(ValueFromPipeline)] $InputObject,
        [string] $Prefix = ''
    )
    $result = [ordered]@{}

    foreach ($prop in $InputObject.PSObject.Properties) {
        $key = if ($Prefix) { "$Prefix.$($prop.Name)" } else { $prop.Name }

        if ($prop.Value -is [PSCustomObject]) {
            # Recurse into nested objects
            $nested = ConvertTo-FlatHashtable -InputObject $prop.Value -Prefix $key
            foreach ($k in $nested.Keys) { $result[$k] = $nested[$k] }
        }
        elseif ($prop.Value -is [System.Collections.IEnumerable] -and $prop.Value -isnot [string]) {
            # Flatten arrays with index suffix
            $i = 0
            foreach ($item in $prop.Value) {
                $result["$key[$i]"] = $item
                $i++
            }
        }
        else {
            $result[$key] = $prop.Value
        }
    }
    $result
}

$json = '{"user":{"name":"Bob","roles":["admin","reader"],"meta":{"created":"2024-01-01"}}}'
$json | ConvertFrom-Json | ConvertTo-FlatHashtable

Streaming Large JSON Files with StreamReader

Loading a 50 MB JSON file with Get-Content | ConvertFrom-Json will allocate the entire string in memory. For line-delimited JSON (NDJSON/JSONL), stream it line by line instead.

$filePath = 'C:\Data\events.jsonl'   # one JSON object per line
$reader   = [System.IO.StreamReader]::new($filePath, [System.Text.Encoding]::UTF8)

try {
    $lineNumber = 0
    while (-not $reader.EndOfStream) {
        $line = $reader.ReadLine()
        $lineNumber++
        if (-not $line.Trim()) { continue }

        try {
            $obj = $line | ConvertFrom-Json -ErrorAction Stop
            # Process $obj here — e.g. filter and export
            if ($obj.severity -eq 'ERROR') {
                $obj | Export-Csv 'C:\Data\errors.csv' -Append -NoTypeInformation
            }
        }
        catch {
            Write-Warning "Line $lineNumber parse error: $_"
        }
    }
}
finally {
    $reader.Dispose()
}

Write-Host "Processed $lineNumber lines."

Converting JSON Arrays to PowerShell Arrays Correctly

A JSON array at the top level deserves special attention because PowerShell’s pipeline can unwrap it unexpectedly, and assigning to a variable does not always produce a predictable type.

$json = '[{"id":1,"name":"Alpha"},{"id":2,"name":"Beta"},{"id":3,"name":"Gamma"}]'

# Safe pattern: wrap in @() to guarantee an array even if JSON contains one element
$items = @($json | ConvertFrom-Json)

Write-Host "Count: $($items.Count)"   # 3
Write-Host "Type:  $($items.GetType().Name)"   # Object[] (not PSCustomObject)

# Access by index
Write-Host $items[0].name   # Alpha

# Filter and pipeline work as expected
$items | Where-Object id -gt 1 | Select-Object name

The @() wrapping is critical when the array might have exactly one element — without it, PowerShell unwraps the single-item array to a scalar and .Count returns 1 for the wrong reason.

Comparing ConvertFrom-Json vs Newtonsoft.Json via .NET

For extremely large files or performance-critical paths, you can call Newtonsoft.Json directly from PowerShell via .NET. This is rarely necessary but worth knowing when ConvertFrom-Json is the bottleneck.

# Load Newtonsoft if available (e.g. from NuGet cache or bundled with app)
$nwtPath = 'C:\Tools\Newtonsoft.Json.dll'
if (Test-Path $nwtPath) {
    Add-Type -Path $nwtPath
    $json   = [System.IO.File]::ReadAllText('C:\Data\large.json')
    $parsed = [Newtonsoft.Json.Linq.JObject]::Parse($json)
    Write-Host "Top-level keys: $(($parsed.Properties() | Measure-Object).Count)"
}
else {
    Write-Warning "Newtonsoft.Json not available — falling back to ConvertFrom-Json"
    $parsed = Get-Content 'C:\Data\large.json' -Raw | ConvertFrom-Json -Depth 100
}

Common Errors and Fixes

-Depth parameter ignored in PowerShell 5.1 — objects truncated silently. The -Depth parameter was not added until PowerShell 6.2. In 5.1, any JSON object nested beyond 100 levels is converted to an empty string without error. Validate your 5.1 output against expected key counts when processing complex API responses, and consider upgrading to PowerShell 7 for production JSON workflows.

Duplicate JSON keys cause last-write-wins behavior without warning. RFC 8259 permits duplicate keys but leaves behavior to implementations. PowerShell’s ConvertFrom-Json silently keeps the last value for duplicate keys in both PSCustomObject and hashtable mode. If your API can return duplicate keys (some GraphQL and legacy REST services do), validate or deduplicate the raw JSON string before parsing.

Related Cmdlets / See Also

Wrapping Up

Master the distinction between PSCustomObject and -AsHashtable output, use @() around top-level arrays, and stream line-delimited files with StreamReader for large payloads. Knowing where PowerShell 5.1’s depth truncation bites you will save hours of debugging mysterious missing fields in deeply nested API responses.

Send-Item -To