PowerShell language allows you to work and build complicated objects. There are multiple ways to save them, such as XML or JSON, but sometimes using them is impossible or inadequate. Sometimes you want to use HTML or CSV or any other single dimension output.
Let's say you have an advanced PowerShell object with nested properties. It's a great feature of PowerShell, but displaying it to the user saving it as CSV or HTML will give you results that will not be very useful.
$Object3 = [PSCustomObject] @{ "Name" = "Przemyslaw Klys" "Age" = "30" "Address" = @{ "Street" = "Kwiatowa" "City" = "Warszawa" "Country" = [ordered] @{ "Name" = "Poland" } List = @( [PSCustomObject] @{ "Name" = "Adam Klys" "Age" = "32" } [PSCustomObject] @{ "Name" = "Justyna Klys" "Age" = "33" } ) } ListTest = @( [PSCustomObject] @{ "Name" = "Justyna Klys" "Age" = "33" } ) } $Object4 = [PSCustomObject] @{ "Name" = "Przemyslaw Klys" "Age" = "30" "Address" = @{ "Street" = "Kwiatowa" "City" = "Warszawa" "Country" = [ordered] @{ "Name" = "Gruzja" } List = @( [PSCustomObject] @{ "Name" = "Adam Klys" "Age" = "32" } [PSCustomObject] @{ "Name" = "Pankracy Klys" "Age" = "33" } [PSCustomObject] @{ "Name" = "Justyna Klys" "Age" = 30 } [PSCustomObject] @{ "Name" = "Justyna Klys" "Age" = $null } ) } ListTest = @( [PSCustomObject] @{ "Name" = "Sława Klys" "Age" = "33" } ) MoreProperties = $true }
$Object3, $Object4 | Out-HtmlView -Filtering
While Name and Age properties output was proper, the rest – not so much.
While you can always use JSON and XML as mentioned to save files – wouldn't it be nice to have another option? This is where ConvertTo-FlatObject comes in! It converts advanced objects into flat ones.
$Object3, $Object4 | ConvertTo-FlatObject | Export-Csv -Path "$PSScriptRoot\test.csv" -NoTypeInformation -Encoding UTF8
The same goes for Out-HTMLView and New-HTMLTable from PSWriteHTML module. As part of the module, I've added a parameter called FlattenObject
which internally uses ConvertTo-FlatObject to make it easy for end-users.
$Object3, $Object4 | Out-HtmlView -Filtering -FlattenObject
If you're wondering what it could be useful for – let me give you an example. Get-AzureADAuditSignInLogs is a cmdlet from Office 365 to get information about logging in users to Office 365.
Connect-AzureAD $Logins = Get-AzureADAuditSignInLogs $Logins[0]
This means that once you push it to CSV or HTML using Out-HTMLView, all those nested properties would be hidden away or mainly unreadable, unsortable.
As you see above – it's far from perfect. I would usually take my PowerShell skills and convert those advanced objects into smaller, one-dimensional objects to display exactly what I wanted. If I had 50 columns, I would rebuild the thing entirely, and it would take some time to do it. Or I would use new functionality and just like that, with a single line of code get it all in one go.
$Logins | Out-HtmlView -ScrollX -Filtering -FlattenObject $Logins | Out-HtmlView -ScrollX -Filtering -FlattenObject -ExcludeProperty "*IsReadOnly","*Count"
Notice that I'm using ExcludeProperty to filter out some columns that are often part of the object that I don't want to see. Isn't it much nicer? Easily readable, zero effort. Report ready in 15 minutes.
ConvertTo-FlatObject is part of the PSSharedGoods PowerShell module. This is my “glue” module with loads of functions. It contains over 200 functions that allow me to reuse those when needed. How do you install it? The easiest and most optimal way is to use PowerShellGallery. This will get you up and running in no time. Whenever there is an update, just run Update-Module, and you're done.
Install-Module PSSharedGoods # Update-Module PSSharedGoods
If you prefer New-HTMLTable or Out-HTMLView commands, you may want to install PSWriteHTML instead.
Install-Module PSWriteHTML # Update-Module PSWriteHTML
However, if you're into code – want to see how everything is done, you can use GitHub sources. Please keep in mind that the PowerShellGallery version is optimized and better for production use. If you see any issues, bugs, or features that are missing, please make sure to submit them on GitHub.
If you don't like using modules, you can use code directly.
Function ConvertTo-FlatObject { <# .SYNOPSIS Flattends a nested object into a single level object. .DESCRIPTION Flattends a nested object into a single level object. .PARAMETER Objects The object (or objects) to be flatten. .PARAMETER Separator The separator used between the recursive property names .PARAMETER Base The first index name of an embedded array: - 1, arrays will be 1 based: <Parent>.1, <Parent>.2, <Parent>.3, … - 0, arrays will be 0 based: <Parent>.0, <Parent>.1, <Parent>.2, … - "", the first item in an array will be unnamed and than followed with 1: <Parent>, <Parent>.1, <Parent>.2, … .PARAMETER Depth The maximal depth of flattening a recursive property. Any negative value will result in an unlimited depth and could cause a infinitive loop. .PARAMETER Uncut The maximal depth of flattening a recursive property. Any negative value will result in an unlimited depth and could cause a infinitive loop. .EXAMPLE $Object3 = [PSCustomObject] @{ "Name" = "Przemyslaw Klys" "Age" = "30" "Address" = @{ "Street" = "Kwiatowa" "City" = "Warszawa" "Country" = [ordered] @{ "Name" = "Poland" } List = @( [PSCustomObject] @{ "Name" = "Adam Klys" "Age" = "32" } [PSCustomObject] @{ "Name" = "Justyna Klys" "Age" = "33" } [PSCustomObject] @{ "Name" = "Justyna Klys" "Age" = 30 } [PSCustomObject] @{ "Name" = "Justyna Klys" "Age" = $null } ) } ListTest = @( [PSCustomObject] @{ "Name" = "Sława Klys" "Age" = "33" } ) } $Object3 | ConvertTo-FlatObject .NOTES Based on https://powersnippets.com/convertto-flatobject/ #> [CmdletBinding()] Param ( [Parameter(ValueFromPipeLine)][Object[]]$Objects, [String]$Separator = ".", [ValidateSet("", 0, 1)]$Base = 1, [int]$Depth = 5, [Parameter(DontShow)][String[]]$Path, [Parameter(DontShow)][System.Collections.IDictionary] $OutputObject ) Begin { $InputObjects = [System.Collections.Generic.List[Object]]::new() } Process { foreach ($O in $Objects) { $InputObjects.Add($O) } } End { If ($PSBoundParameters.ContainsKey("OutputObject")) { $Object = $InputObjects[0] $Iterate = [ordered] @{} if ($null -eq $Object) { #Write-Verbose -Message "ConvertTo-FlatObject - Object is null" } elseif ($Object.GetType().Name -in 'String', 'DateTime', 'TimeSpan', 'Version', 'Enum') { $Object = $Object.ToString() } elseif ($Depth) { $Depth-- If ($Object -is [System.Collections.IDictionary]) { $Iterate = $Object } elseif ($Object -is [Array] -or $Object -is [System.Collections.IEnumerable]) { $i = $Base foreach ($Item in $Object.GetEnumerator()) { $Iterate["$i"] = $Item $i += 1 } } else { foreach ($Prop in $Object.PSObject.Properties) { if ($Prop.IsGettable) { $Iterate["$($Prop.Name)"] = $Object.$($Prop.Name) } } } } If ($Iterate.Keys.Count) { foreach ($Key in $Iterate.Keys) { ConvertTo-FlatObject -Objects @(, $Iterate["$Key"]) -Separator $Separator -Base $Base -Depth $Depth -Path ($Path + $Key) -OutputObject $OutputObject } } else { $Property = $Path -Join $Separator $OutputObject[$Property] = $Object } } elseif ($InputObjects.Count -gt 0) { foreach ($ItemObject in $InputObjects) { $OutputObject = [ordered]@{} ConvertTo-FlatObject -Objects @(, $ItemObject) -Separator $Separator -Base $Base -Depth $Depth -Path $Path -OutputObject $OutputObject [PSCustomObject] $OutputObject } } } }