'Understanding Powershell: example - Convert JSON to CSV

I've read several posts (like Convert JSON to CSV using PowerShell) regarding using PowerShell to CSV. I have also read that it is relatively poor form to use the pipe syntax in scripts -- that it's really meant for command line and can create a hassle for developers to maintain over time.

Using this sample JSON file...

[
    {
        "a":  "Value 1",
        "b":  20,
        "g":  "Arizona"
    },
    {
        "a":  "Value 2",
        "b":  40
    },
    {
        "a":  "Value 3"
    },
    {
        "a":  "Value 4",
        "b":  60
    }
]

...this code...

((Get-Content -Path $pathToInputFile -Raw) | ConvertFrom-Json) | Export-CSV $pathToOutputFile -NoTypeInformation

...creates a file containing CSV as expected.

"a","b","g"
"Value 1","20","Arizona"
"Value 2","40",
"Value 3",,
"Value 4","60",

This code...

$content = Get-Content -Path $pathToInputFile -Raw
$psObj = ConvertFrom-Json -InputObject $content
Export-Csv -InputObject $psObj -LiteralPath $pathToOutputFile -NoTypeInformation

...creates a file containing nonsense:

"Count","Length","LongLength","Rank","SyncRoot","IsReadOnly","IsFixedSize","IsSynchronized"
"4","4","4","1","System.Object[]","False","True","False"

It looks like maybe an object definition(?).
What is the difference? What PowerShell nuance did I miss when converting the code?

The answer to Powershell - Export a List of Objects to CSV says the problem is from the -InputObject option causing the object, not it's contents, to be sent to Export-Csv, but doesn't state how to remedy the problem without using the pipe syntax. I'm thinking something like -InputObject $psObj.contents. I realize that's not a real thing, but I Get-Members doesn't show me anything that looks like it will solve this.



Solution 1:[1]

This is not meant as an answer but just to give you a vague representation of what ConvertTo-Csv and Export-Csv are doing and to help you understand why -InputObject is meant to be bound from the pipeline and should not be used manually.

function ConvertTo-Csv2 {
    param(
        [parameter(ValueFromPipeline)]
        [Object] $InputObject
    )

    begin {
        $isFirstObject = $true
        filter Normalize {
            if($_ -match '"') { return $_.Replace('"','""') }
            $_
        }
    }

    process {
        if($isFirstObject) {
            $headers = $InputObject.PSObject.Properties.Name | Normalize
            $isFirstObject = $false
            [string]::Format('"{0}"', [string]::Join('","', $headers))
        }

        $values = foreach($value in $InputObject.PSObject.Properties.Value) {
            $value | Normalize
        }
        [string]::Format('"{0}"', [string]::Join('","', $values))
    }
}

As we can observe, there is no loop enumerating the $InputObject in the process block of this function, yet, because of how this block works, each object coming from the pipeline is processed and converted to a Csv string representation of the object.

Within a pipeline, the Process block executes once for each input object that reaches the function.

If instead, we attempt to use the InputObject parameter from the function, the object being passed as argument will be processed only once.

Calling the function at the beginning, or outside of a pipeline, executes the Process block once.

Solution 2:[2]

Get-Members doesn't show me anything that looks like it will solve this

Get-Member

It's because how you pass values has different behavior.

The pipeline enumerates values, it's almost like a foreach($item in $pipeline). Passing by Parameter skips that

Here I have an array of 3 letters.

$Letters = 'a'..'c'

I'm getting different types

Get-Member -InputObject $Letters
# [Object[]]

# [char]
$letters | Get-Member

Processed for each item

$letters | ForEach-Object { 
    "iteration: $_"
}
iteration: a
iteration: b
iteration: c

Compare to

ForEach-Object -InputObject $Letters { 
    "iteration: $_"
}
iteration: a b c

Detecting types

Here's a few ways to inspect objects.

using ClassExplorer

PS> ($Letters).GetType().FullName
PS> ($Letters[0]).GetType().FullName  # first child

    System.Object[]
    System.Char

PS> $Letters.count
PS> $Letters[0].Count

    3
    1

$Letters.pstypenames -join ', '
$Letters[0].pstypenames -join ', '

    System.Object[], System.Array, System.Object
    System.Char, System.ValueType, System.Object

Tip: $null.count always returns 0. It does not throw an error.

if($neverExisted.count -gt 1) { ... }

Misc

I have also read that it is relatively poor form to use the pipe syntax in scripts

This is not true, Powershell is designed around piping objects.

Tip: $null.count always returns 0. It does not throw an error.

Maybe They were talking about

Example2: slow operations

Some cases when you need something fast, the overhead to Foreach-Object over a foreach can be an issue. It makes it so you have to use some extra syntax.

If you really need speed, you should probably be calling dotnet methods anyway.

Example1: Piping when you could use a parameter

I'm guessing they meant piping a variable in cases where you can pass parameters?

$text = "hi-world"

# then
$text | Write-Host 

# vs
Write-Host -InputObject $Text

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2