Deep Object Comparisons

Learn ways that you can do deep comparisons of objects in PowerShell

Background

I was recently asked (at least, that’s what I chose to respond about) how you could do a deep comparison of objects (especially in 2 arrays) in PowerShell. The problem is that most built in ways (Linq, Compare-Object) will only work for objects that are only 2 layers deep. Further layers of depth will just .ToString() and not be counted in the comparison.

I encountered a similar situation when I was making a DSC-esque way to manage Discord application commands (which can have MANY layers of objects via options and embeds). Hopefully this post will give you some ideas on what you can do.

Seeing the Problem

Note how this works with Compare-Object:

$array1 = @(
    [PSCustomObject]@{color = "blue"; size = "large" }
    [PSCustomObject]@{color = "red"; size = "large" }
    [PSCustomObject]@{color = "red"; size = "large" }
    [PSCustomObject]@{color = "red"; size = "small" }
)
$array2 = @(
    [PSCustomObject]@{color = "blue"; size = "large" }
    [PSCustomObject]@{color = "red"; size = "large" }
    [PSCustomObject]@{color = "green"; size = "large" }
)

$properties = ($array1 | Get-Member -MemberType NoteProperty).Name
Compare-Object $array1 $array2 -Property $properties

Output:

color size  SideIndicator
----- ----  -------------
green large =>
red   large <=
red   small <=

It works perfect! Compare-Object is just great.

But you get problems when you have deeper objects:

$array1 = @(
    [PSCustomObject]@{color = "blue"; size = "large"; density = @{volume = 3; weight = 3 } }
    [PSCustomObject]@{color = "red"; size = "large" }
    [PSCustomObject]@{color = "red"; size = "large" }
    [PSCustomObject]@{color = "red"; size = "small" }
)
$array2 = @(
    [PSCustomObject]@{color = "blue"; size = "large"; density = @{volume = 3; weight = 4 } }
    [PSCustomObject]@{color = "red"; size = "large" }
    [PSCustomObject]@{color = "green"; size = "large" }
)

$properties = ($array1 | Get-Member -MemberType NoteProperty).Name
Compare-Object $array1 $array2 -Property $properties -IncludeEqual

See here how it calls the first item in both sets as “equal” despite the fact that their density weight property is different.

color density          size  SideIndicator
----- -------          ----  -------------
blue  {volume, weight} large ==
red                    large ==
green                  large =>
red                    large <=
red                    small <=

That’s no good! Try getting a deep object representation via Json!

Using JSON to Represent Deep Objects

If you pass in a scriptblock that turns each deep object into a json string, then Compare-object can do a simple string comparison to determine equivalence. Something like this:

Compare-Object $array1 $array2 -Property { $_ | ConvertTo-Json -Depth 10 -Compress }

And the output then looking like this (you could instead do -Passthru to get the original object in the pipeline of course):

$_ | ConvertTo-Json -Depth 10 -Compress                          SideIndicator
-----------------------------------------                         -------------
{"color":"blue","size":"large","density":{"volume":3,"weight":4}} =>
{"color":"green","size":"large"}                                  =>
{"color":"blue","size":"large","density":{"volume":3,"weight":3}} <=
{"color":"red","size":"large"}                                    <=
{"color":"red","size":"small"}                                    <=

This works great as long as all properties are in the SAME property order.

The first item in each array is equal. But the ordering on the density cause it to fail in JSON string comparison:

$array1 = @(
    [PSCustomObject]@{color = "blue"; size = "large"; density = [ordered]@{volume = 3; weight = 3 } }
    [PSCustomObject]@{color = "red"; size = "large" }
    [PSCustomObject]@{color = "red"; size = "large" }
    [PSCustomObject]@{color = "red"; size = "small" }
)
$array2 = @(
    [PSCustomObject]@{color = "blue"; size = "large"; density = [ordered]@{weight = 3; volume = 3 } }
    [PSCustomObject]@{color = "red"; size = "large" }
    [PSCustomObject]@{color = "green"; size = "large" }
)

Compare-Object $array1 $array2 -Property { $_ | ConvertTo-Json -Depth 10 -Compress }

Output:

$_ | ConvertTo-Json -Depth 10 -Compress                          SideIndicator
-----------------------------------------                         -------------
{"color":"blue","size":"large","density":{"weight":3,"volume":3}} =>
{"color":"green","size":"large"}                                  =>
{"color":"blue","size":"large","density":{"volume":3,"weight":3}} <=
{"color":"red","size":"large"}                                    <=
{"color":"red","size":"small"}                                    <=

Hmmm, you want the JSON key ordering (which doesn’t actually represent any differences in a PowerShell object) to not mess with the comparison.

Comparing Via JSON Representation With Sorted Keys

In this pattern, what you need to do is sort the JSON keys for each object and then use THAT output for comparison purposes. I’ve found that the best way to do that is via the Convert-JsonKeysToSorted function from the JsonUtils module (full disclosure, I’m now the maintainer of that module).

Which can be seen like so:

Compare-Object $array1 $array2 -IncludeEqual -Property {
    $_ | ConvertTo-Json -Depth 10 -Compress | Convert-JsonKeysToSorted -Depth 10 -Compress
}

And output:

$_ | ConvertTo-Json -Depth 10 -Compress | Convert-JsonKeysToSorted -Depth 10 -Compress  SideIndicator
---------------------------------------------------------------------------------------- -------------
{"color":"blue","density":{"volume":3,"weight":3},"size":"large"}                        ==
{"color":"red","size":"large"}                                                           ==
{"color":"green","size":"large"}                                                         =>
{"color":"red","size":"large"}                                                           <=
{"color":"red","size":"small"}                                                           <=

Now you really have something that takes any object, as long as it is 10 or less layers deep, and can do a nice json comparison between them. Case insensitive since string comparisons in powershell are case insensitive by default.

Making It Pretty

If you then just want the original object back, use passthru. And also be sure to add in some splatting. Like so:

$json_params = @{
    Depth    = 10
    Compress = $true
}
Compare-Object $array1 $array2 -PassThru -Property {
    $_ | ConvertTo-Json @json_params | Convert-JsonKeysToSorted @json_params
}

Output:

color size  SideIndicator
----- ----  -------------
green large =>
red   large <=
red   small <=

Probably split based on SideIndicator and then do a | Select * -Exclude SideIndicator too. Boom, back to original schema.

If you are going to do multiple comparisons on them, I’d suggest adding a new property to each object called like “Comparer” that you use in Compare-Object -Property on. Saves you from running this same block on the code every time!

Wrapping Up

Now you see how you can turn almost any deep object in PowerShell into a JSON string and use that to compare with any number of other representations.

Be careful though, comparing arrays with more than 100k-1MM (million) items in it will likely take too long in this method to be useful.

Discussion

If you want to drop me a note about this, you can do so on my original comment on Reddit.