PowerShell

PowerShell – Converting advanced object to flat object

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.

HTML Export for nested objects

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.

Export-CSV for nested objects

The same problem will be visible in the Export-CSV command.

$Object3, $Object4 | Export-Csv -Path "$PSScriptRoot\test.csv" -NoTypeInformation

Converting Advanced PowerShell object to flat objects

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

Office 365 Example of using ConvertTo-FlatObject inside Out-HTMLView

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.

Installing PSSharedGoods

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.

Direct source code for ConvertTo-FlatObject

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
            }
        }
    }
}

Przemyslaw Klys

System Architect with over 14 years of experience in the IT field. Skilled, among others, in Active Directory, Microsoft Exchange and Office 365. Profoundly interested in PowerShell. Software geek.

Share
Published by
Przemyslaw Klys

Recent Posts

Upgrade Azure Active Directory Connect fails with unexpected error

Today, I made the decision to upgrade my test environment and update the version of…

6 days ago

Mastering Active Directory Hygiene: Automating Stale Computer Cleanup with CleanupMonster

Have you ever looked at your Active Directory and wondered, "Why do I still have…

4 months ago

Active Directory Replication Summary to your Email or Microsoft Teams

Active Directory replication is a critical process that ensures the consistent and up-to-date state of…

8 months ago

Syncing Global Address List (GAL) to personal contacts and between Office 365 tenants with PowerShell

Hey there! Today, I wanted to introduce you to one of the small but excellent…

1 year ago

Active Directory Health Check using Microsoft Entra Connect Health Service

Active Directory (AD) is crucial in managing identities and resources within an organization. Ensuring its…

1 year ago

Seamless HTML Report Creation: Harness the Power of Markdown with PSWriteHTML PowerShell Module

In today's digital age, the ability to create compelling and informative HTML reports and documents…

1 year ago