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.