{"id":92359,"date":"2021-09-03T09:12:48","date_gmt":"2021-09-03T09:12:48","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=92359"},"modified":"2021-11-05T12:18:50","modified_gmt":"2021-11-05T12:18:50","slug":"diff-objects-a-powershell-utility-cmdlet-for-discovering-object-differences","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/blogs\/diff-objects-a-powershell-utility-cmdlet-for-discovering-object-differences\/","title":{"rendered":"Deep Object Comparison in PowerShell: A Custom Diff-Objects Cmdlet for Nested Property Differences (Beyond Compare-Object)"},"content":{"rendered":"<p><b>PowerShell&#8217;s built-in Compare-Object cmdlet compares two collections of objects by overall equality &#8211; it tells you which objects are in one set but not the other, using .Equals() for comparison. For simple object types (strings, numbers, primitives), this works correctly. For complex objects with nested properties (like AD users with multi-valued attributes, SQL Server configuration objects with sub-collections, or custom PSCustomObjects with arrays and nested objects), Compare-Object compares the objects as wholes &#8211; it doesn&#8217;t traverse INTO them to find WHICH properties differ. This article presents a Diff-Objects custom cmdlet that does the deeper comparison: given two objects, it recursively walks every property, identifies differences at each level, and returns a detailed report (Property: &#8216;Address.Street&#8217;, Reference: &#8216;Old Street&#8217;, Difference: &#8216;New Street&#8217;). Use cases: configuration drift detection (compare last-known-good config to current), AD\/Azure-AD user auditing (what changed between two snapshots), API response comparison (verify expected vs actual), PowerShell scripting test assertions (deeper than Compare-Object&#8217;s output). The article includes complete cmdlet source, usage examples, and notes on handling edge cases (null values, circular references, array comparison semantics).<\/b><\/p>\n<h2>Introduction<\/h2>\n<p>It is important for many development jobs to be able to look at the differences between PowerShell objects.<\/p>\n<p>It might be that you are checking how Windows processes change over time: Perhaps you are monitoring various signs of stress on a server but are at the exploratory stage, where you need to see the metrics that seem to be correlating: You might be wanting to make a high-level check to changes to a database by comparing the metadata to see what has changed. I use it to get a \u2018narrative of changes\u2019 in databases under development, and roughly when they happened.<\/p>\n<p>One of the most common things you need to be able to do when developing stuff is to be able to do automated unit tests. This means detecting automatically the actual output with the correct output.<\/p>\n<p>Whatever your development methodology, you need to make changes lightning fast, and the easiest way of doing that is to test frequently. If you are driving this work with PowerShell, which works well, you\u2019ll want to compare the actual results of a process with the expected results. You\u2019re keen to see what\u2019s changed but will often have no idea what to look for beforehand. You need the broad view.<\/p>\n<p>Fine. To do this, you need something that can tell you the differences between two objects. Yes, there is already a cmdlet to do that called Compare-Object. It is useful and ingenious, and works well for what is does, but it doesn\u2019t do enough for our purposes. Let\u2019s run through a few examples, just to explain why I need more for many of the things I do.<\/p>\n<h2>Using PowerShell\u2019s Compare-Objects.<\/h2>\n<p>We first create two simple objects which have differences and similarities. They are strings which are system.objects.<\/p>\n<pre class=\"theme:powershell-ise font:consolas font-size:13 line-height:12 marking:false ranges:false nums-toggle:false wrap:true wrap-toggle:false popup:false scroll:false tab-size:3 lang:ps decode:true\">@'\r\nAlthough the Borgias\r\nwere rather gorgeous\r\nthey liked the absurder\r\nkinds of murder\r\n\r\nAnon\r\n'@&gt;\"$env:Temp\\Firstpoem.txt\"\r\n@'\r\nHere lies John Bunn\r\nwho was killed by a gun\r\nhis name wasn't bun but Wood\r\n'Wood' wouldn't rhyme with gun but 'Bunn' would\r\n\r\nAnon\r\n'@&gt;\"$env:Temp\\Secondpoem.txt\"\r\n<\/pre>\n<p>Now we compare these two objects<\/p>\n<pre class=\"theme:powershell-ise font:consolas font-size:13 line-height:12 marking:false ranges:false nums-toggle:false wrap:true wrap-toggle:false popup:false scroll:false tab-size:3 lang:ps decode:true\">compare-object (Get-Content \"$env:Temp\\poem.txt\")(Get-Content \"$env:Temp\\Secondpoem.txt\") -IncludeEqual<\/pre>\n<pre class=\"theme:powershell-output font:consolas font-size:13 line-height:12 marking:false ranges:false nums-toggle:false wrap-toggle:false popup:false scroll:false tab-size:3 lang:ps decode:true\">InputObject                                     SideIndicator\r\n-----------                                     -------------\r\n                                                ==           \r\nAnon                                            ==           \r\nHere lies John Bunn                             =&gt;           \r\nwho was killed by a gun                         =&gt;           \r\nhis name wasn't bun but Wood                    =&gt;           \r\n'Wood' wouldn't rhyme with gun but 'Bunn' would =&gt;           \r\nAlthough the Borgias                            &lt;=           \r\nwere rather gorgeous                            &lt;=           \r\nthey liked the absurder                         &lt;=           \r\nkinds of murder                                 &lt;=           <\/pre>\n<p>OK. What that means is that \u2018Anon\u2019 and the blank line are in both equal in both (==) , and the rest are either just in the First poem (&lt;=) or else in the second (=&gt;)<\/p>\n<p>I\u2019d much prefer a side-by-side comparison that you can filter to just show the differences<\/p>\n<pre class=\"theme:powershell-output font:consolas font-size:13 line-height:12 marking:false ranges:false nums-toggle:false wrap-toggle:false popup:false scroll:false tab-size:3 lang:ps decode:true\">Ref  Source                  Target                                          Match\r\n---  ------                  ------                                          -----\r\n$[0] Although the Borgias    Here lies John Bunn                             &lt;&gt;   \r\n$[1] were rather gorgeous    who was killed by a gun                         &lt;&gt;   \r\n$[2] they liked the absurder his name wasn't bun but Wood                    &lt;&gt;   \r\n$[3] kinds of murder         'Wood' wouldn't rhyme with gun but 'Bunn' would &lt;&gt;   \r\n$[4]                                                                         ==   \r\n$[5] Anon                    Anon                                            ==   \r\n<\/pre>\n<p>OK. Let\u2019s give it something more complicated.<\/p>\n<pre class=\"theme:powershell-ise font:consolas font-size:13 line-height:12 marking:false ranges:false nums-toggle:false wrap:true wrap-toggle:false popup:false scroll:false tab-size:3 lang:ps decode:true\">$TheResult= #our first list of employees\r\n@'\r\n{\r\n    \"employees\":  [\r\n                      {\r\n                          \"resigned\":  \"false\",\r\n                          \"salary\":  275,\r\n                          \"lastName\":  \"Doe\",\r\n                          \"Enddate\":  null,\r\n                          \"warnings\":  [234, 678,3453, 67],\r\n                          \"firstName\":  \"John\"\r\n                      },\r\n                      {\r\n                          \"EndDate\":  \"2012-04-23T18:25:43.511Z\",\r\n                          \"resigned\":  false,\r\n                          \"firstName\":  \"Anna\",\r\n                          \"salary\":  300,\r\n                          \"lastName\":  \"Smith\"\r\n                      },\r\n                      {\r\n                          \"EndDate\":  \"2018-04-23T18:25:43.511Z\",\r\n                          \"resigned\":  false,\r\n                          \"firstName\":  \"Peter\",\r\n                          \"salary\":  400,\r\n                          \"lastName\":  \"Jones\"\r\n                      }\r\n                  ]\r\n}\r\n'@ | convertFrom-json\r\n$TheOtherResult= #our revised  list of employees\r\n@'\r\n{\r\n    \"employees\":  [\r\n                      {\r\n                          \"resigned\":  \"true\",\r\n                          \"salary\":  275,\r\n                          \"lastName\":  \"Doe\",\r\n                          \"Enddate\":  null,\r\n                          \"warnings\":  [234, 678,3453,56, 67],\r\n                          \"firstName\":  \"John\"\r\n                      },\r\n                      {\r\n                          \"EndDate\":  \"2012-04-23T18:25:43.511Z\",\r\n                          \"resigned\":  false,\r\n                          \"firstName\":  \"Anna\",\r\n                          \"salary\":  350,\r\n                          \"lastName\":  \"Smith\"\r\n                      },\r\n                      {\r\n                          \"EndDate\":  \"2018-04-23T18:25:43.511Z\",\r\n                          \"resigned\":  false,\r\n                          \"firstName\":  \"Peter\",\r\n                          \"salary\":  400,\r\n                          \"lastName\":  \"Jones\"\r\n                      }\r\n                  ]\r\n}\r\n'@ | convertFrom-json\r\n\r\n#So we ask PowerShell what the differences are\r\nCompare-object -ReferenceObject $TheResult.employees `\r\n    -DifferenceObject $TheOtherResult.employees `\r\n    -IncludeEqual `\r\n    -Property @('salary','Firstname','Lastname','Enddate','warnings','Resigned') \r\n<\/pre>\n<p>Well. We get this\u2026<\/p>\n<pre class=\"theme:powershell-output font:consolas font-size:13 line-height:12 marking:false ranges:false nums-toggle:false wrap-toggle:false popup:false scroll:false tab-size:3 lang:ps decode:true\">salary        : 400\r\nFirstname     : Peter\r\nLastname      : Jones\r\nEnddate       : 2018-04-23T18:25:43.511Z\r\nwarnings      : \r\nResigned      : False\r\nSideIndicator : ==\r\n\r\nsalary        : 275\r\nFirstname     : John\r\nLastname      : Doe\r\nEnddate       : \r\nwarnings      : {234, 678, 3453, 56...}\r\nResigned      : true\r\nSideIndicator : =&gt;\r\n\r\nsalary        : 350\r\nFirstname     : Anna\r\nLastname      : Smith\r\nEnddate       : 2012-04-23T18:25:43.511Z\r\nwarnings      : \r\nResigned      : False\r\nSideIndicator : =&gt;\r\n\r\nsalary        : 275\r\nFirstname     : John\r\nLastname      : Doe\r\nEnddate       : \r\nwarnings      : {234, 678, 3453, 67}\r\nResigned      : false\r\nSideIndicator : &lt;=\r\n\r\nsalary        : 300\r\nFirstname     : Anna\r\nLastname      : Smith\r\nEnddate       : 2012-04-23T18:25:43.511Z\r\nwarnings      : \r\nResigned      : False\r\nSideIndicator : &lt;= \r\n<\/pre>\n<p>Which tells us that two objects in the array are the same (==), two are only in the second object (=&gt;) and two are in the first (&lt;=). However, it doesn\u2019t tell us that Anna\u2019s salary is the difference. To get into the detail, I want something like this.<\/p>\n<pre class=\"theme:powershell-output font:consolas font-size:13 line-height:12 marking:false ranges:false nums-toggle:false wrap-toggle:false popup:false scroll:false tab-size:3 lang:ps decode:true\">Ref                        Source                   Target                   Match\r\n---                        ------                   ------                   -----\r\n$.employees[0].firstName   John                     John                     ==   \r\n$.employees[0].lastName    Doe                      Doe                      ==   \r\n$.employees[0].resigned    false                    true                     &lt;&gt;   \r\n$.employees[0].salary      275                      275                      ==   \r\n$.employees[0].warnings[0] 234                      234                      ==   \r\n$.employees[0].warnings[1] 678                      678                      ==   \r\n$.employees[0].warnings[2] 3453                     3453                     ==   \r\n$.employees[0].warnings[3] 67                       56                       &lt;&gt;   \r\n$.employees[0].warnings[4]                          67                       -&gt;   \r\n$.employees[1].EndDate     2012-04-23T18:25:43.511Z 2012-04-23T18:25:43.511Z ==   \r\n$.employees[1].firstName   Anna                     Anna                     ==   \r\n$.employees[1].lastName    Smith                    Smith                    ==   \r\n$.employees[1].resigned    False                    False                    ==   \r\n$.employees[1].salary      300                      350                      &lt;&gt;   \r\n$.employees[2].EndDate     2018-04-23T18:25:43.511Z 2018-04-23T18:25:43.511Z ==   \r\n$.employees[2].firstName   Peter                    Peter                    ==   \r\n$.employees[2].lastName    Jones                    Jones                    ==   \r\n$.employees[2].resigned    False                    False                    ==   \r\n<\/pre>\n<p>I want a \u2018diff\u2019 or difference of the entire object to see what values have changed. I\u2019d quite like to specify the depth to compare, and what to exclude or include, especially with larger objects. You\u2019ll notice that this result can be filtered to allow you to list just properties that are different or missing.<\/p>\n<h2>Problems with object comparisons.<\/h2>\n<h3>Comparing arrays<\/h3>\n<p>Before we delve too far into how Diff-objects works, we need to mention a general problem with comparing arrays. Whereas, when values have unique keys it is easy to determine differences, There is a whole branch of computer science to work out how, without unique keys, you can compare two versions of an array in a way that shows how it has changed. One of the problems is that the insertion, rather the appending, of an array element makes every subsequent element different to the reference. In terms of text represented by arrays of lines, a carriage return makes everything after it a difference. Where the elements in an array are ordered, and the order is significant, it is legitimate to do this. Where they are random, and you allow duplicate values, then you must match them iteratively, one pair at a time, and then remove them as candidates for the subsequent matches.<\/p>\n<p>In JSON, the order of arrays is significant, so I take this as a precedent to do the easy option.<\/p>\n<h3>Comparing nulls<\/h3>\n<p>NULLs have a variety of meanings. They can mean \u2018Unknown\u2019, which means that the result of comparing an unknown with something is always unknown. There again, a blank string is often used as if it were a NULL string. PowerShell can get confusing because it is very difficult to compare an object that doesn\u2019t exist, and therefore returns NULL when you reference it, with an object that has a null value when you reference it. When comparing objects and their values, you need to know the difference.<\/p>\n<p>I can\u2019t find a consensus view on whether a blank string is the same as a null value. It isn\u2019t in relational databases. One is known to be a blank string and the other is unknown. I\u2019ve made it configurable. If you need to equate null and Blank, just set &#8211;<strong>NullAndBlankSame <\/strong>to <strong>$true. <\/strong>This is useful where you store objects as CSV because CSV has no consistent concept of a null string.<\/p>\n<h3>Avoiding stuff<\/h3>\n<p>Starting a comparison at some reference point is easy: you just specify the reference point where you start the comparison in the <strong>-Parent<\/strong> parameter. Ignoring embedded objects is trickier. A classical example is ignoring comments in XML files. (#comment). A far worst problem is presented by those monster arrays and god-like objects that you tend to find in .NET objects passed to you from the operating system. You can specify a list (array) of strings with the names of the objects or references (those strings in the first column) that you wish to avoid, such as <strong>\u2018$.employees[2].resigned<\/strong>\u2018 in the last example.<\/p>\n<h2>How Diff-Objects works<\/h2>\n<p>I\u2019ve done a blog post describing a Cmdlet I\u2019ve called <a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/display-object-a-powershell-utility-cmdlet\/\">Display-Object<\/a>. Although I\u2019ve found it to be a very useful Cmdlet in its own right, I felt that it was a useful stepping-stone in understanding how I\u2019ve tackled the problem of \u2018diffing\u2019 objects (finding the difference between them). I started writing <strong>Display-Object<\/strong> just as an illustration but, as so often in life, I got rather interested in it because it proved so useful to me in finding out what was going on inside some tricky objects.<\/p>\n<p>Basically, a lot of object comparisons just \u2018walk\u2019 the hierarchy of a reference object, comparing any \u2018comparable object\u2019 (most simple values) with the same reference in the difference object This is like a left outer join. Because I want to see additions and subtractions I do the equivalent of a full outer join to find the differences<\/p>\n<p>It doesn\u2019t report what objects are different, just the values. If an object has differences in the values between the reference object and its equivalent in the difference object then you can be sure there is a difference. By \u2018difference\u2019, I include those records that appear only in one of the two objects.<\/p>\n<p>Any useful Cmdlet that is designed to participate in a pipeline need to report the result of a comparison via a collection of psCustomObjects. In this way, one can use <strong>Select-Object<\/strong>, <strong>Where-Object<\/strong> and all those other useful participants. Although it introduces some redundancies, I use a four-column format.<\/p>\n<ol>\n<li>The first column is the path expression to the object. By this I mean the dot references and array indices. A dollar sign means the name of the object, as with most object paths. Basically, in PowerShell in the ISE, you add the reference except for the dollar sign to the variable referring to the object, execute it, and you\u2019ll see the value.<\/li>\n<li>The second column is the value in the reference object<\/li>\n<li>The Third column is the value in the difference object.<\/li>\n<li>The fourth column contains a symbol that gives the result of the comparison. This can be\n<ol>\n<li>\u2018Both there and equal\u2019 <strong>(\u2018==\u2019) <\/strong><\/li>\n<li>\u2018Both there and different\u2019 <strong>(\u2018&lt;&gt;\u2019)<\/strong><\/li>\n<li>\u2018Only in the difference object\u2019 <strong>(\u2018-&gt;\u2019)<\/strong><\/li>\n<li>\u2018Only in the reference object\u2019 <strong>(\u2018&lt;-\u2019)<\/strong><\/li>\n<li>\u2018Could not be compared (e.g. write-only values)<strong> (\u2018&#8211;\u2019)<\/strong><\/li>\n<\/ol>\n<\/li>\n<\/ol>\n<h1>Uses for an \u2018Object Diff\u2019<\/h1>\n<h2>Checking test results<\/h2>\n<p>Most Cmdlets produce objects. These are often lists of PS Custom Objects. If you can look at the output in Format-Table, that\u2019s probably the case. Unless they are huge, these objects can be rendered as a document that can be saved. The ConvertTo \u2026 series of cmdlets are good for this. If the data is essentially tabular you can save it in its most economical form as CSV, but JSON is OK. This often gives you the opportunity to test your cmdlets as you develop them. You work out, and get general agreement about, what the result should look like for a particular set of parameters. If your cmdlet produces the same result for the same parameters, then you have a degree of confidence that you haven\u2019t broken anything.<\/p>\n<p>Normally, you\u2019d want to keep your test materials on-disk and iterate through them. Just to illustrate how it works, though, I\u2019ve done it in code. I\u2019ve create a file-based dataset that represents what the Display-object Cmdlet actually should be producing, together with the object that we\u2019re displaying. We want to make sure that Display-object still works after we alter it.<\/p>\n<pre class=\"theme:powershell-ise font:consolas font-size:13 line-height:12 marking:false ranges:false nums-toggle:false wrap:true wrap-toggle:false popup:false scroll:false tab-size:3 lang:ps decode:true\">#A Test for a Display-Object Cmdlet that we are developing.\r\n#We have the reference version of what the data should be in #ref\r\n$Ref=@'\r\n#TYPE System.Management.Automation.PSCustomObject\r\n\"Path\",\"Value\"\r\n\"$.Ham.Downtime\",\r\n\"$.Ham.Location\",\"Floor two rack\"\r\n\"$.Ham.Users[0]\",\"Fred\"\r\n\"$.Ham.Users[1]\",\"Jane\"\r\n\"$.Ham.Users[2]\",\"Mo\"\r\n\"$.Ham.Users[3]\",\"Phil\"\r\n\"$.Ham.Users[4]\",\"Tony\"\r\n\"$.Ham.version\",\"2019\"\r\n\"$.Japeth.Location\",\"basement rack\"\r\n\"$.Japeth.Users[0]\",\"Karen\"\r\n\"$.Japeth.Users[1]\",\"Wyonna\"\r\n\"$.Japeth.Users[2]\",\"Henry\"\r\n\"$.Japeth.version\",\"2008\"\r\n\"$.Shem.Location\",\"Server room\"\r\n\"$.Shem.Users[0]\",\"Fred\"\r\n\"$.Shem.Users[1]\",\"Jane\"\r\n\"$.Shem.Users[2]\",\"Mo\"\r\n\"$.Shem.version\",\"2017\"\r\n'@ |ConvertFrom-Csv\r\n# We now have the reference result. we now create the test input \r\n$ServersAndUsers =\r\n@{'Shem' =\r\n  @{\r\n    'version' = '2017'; 'Location' = 'Server room';\r\n        'Users'=@('Fred','Jane','Mo')\r\n     }; \r\n  'Ham' =\r\n  @{\r\n    'version' = '2019'; 'Location' = 'Floor two rack';\r\n        'Downtime'=$null\r\n        'Users'=@('Fred','Jane','Mo','Phil','Tony')\r\n  }; \r\n  'Japeth' =\r\n  @{\r\n    'version' = '2008'; 'Location' = 'basement rack';\r\n        'Users'=@('Karen','Wyonna','Henry')\r\n  }\r\n}\r\n#we run the 'Display-Object' that we are developing.\r\n$Diff= Display-Object $ServersAndUsers\r\n# we now have a #Ref object with what the output should be, and we have the $diff object\r\n# of what is produced by the current version \r\n# We test to see if the $Ref and $Diff match.\r\n$TestResult=Diff-Objects -Ref $ref -Diff $diff -NullAndBlankSame $True |\r\n    where {$_.Match -ne '=='}\r\nif ($TestResult) #if any differences were reported.\r\n    {Write-warning 'Test for Display-Object with  ServersAndUsers failed'\r\n    $TestResult|format-table}\r\n<\/pre>\n<p>You can run this, but instead of changing the code in Diff-object, we can take the easier route and simply change the data and seeing if this is picked up by tester.<\/p>\n<h2>Seeing how data in large objects change.<\/h2>\n<p>The important point here with a large object is to only look at what you are interested in. Even a conceptually-simple object like a data table can end up with a lot of nooks and crannies full of data. A process object can be severely over-weight. You can start by just surveying the branch you\u2019re interested in by specifying the \u2018dot\u2019 address of the data that you need to see, and avoid all the data-carbohydrates. Here, in the first example, we are checking the process where you aren\u2019t at all interested in those arrays, so you filter them out by listing them in the \u2018avoid\u2019 parameter..<\/p>\n<pre class=\"theme:powershell-ise font:consolas font-size:13 line-height:12 marking:false ranges:false nums-toggle:false wrap:true wrap-toggle:false popup:false scroll:false tab-size:3 lang:ps decode:true\">$process=(get-process pwsh)\r\n#&lt;some time later&gt;\r\nDiff-Objects  $process (get-process pwsh) -Depth 3 -Avoid @('Modules','Threads','StartInfo') -NullAndBlankSame $true\r\n<\/pre>\n<p>You can start \u2018some way from the trunk\u2019 by presenting the cmdlet with the same reference, and providing the parentage to the \u2018parent\u2019 parameter so that the reference is correct if you subsequently want to get an individual value. Notice that we\u2019ve not only carved off the branch of the data we\u2019re interested in but we\u2019ve specified this address as \u2018parent\u2019 so that the address is correct too.<\/p>\n<pre class=\"theme:powershell-ise font:consolas font-size:13 line-height:12 marking:false ranges:false nums-toggle:false wrap:true wrap-toggle:false popup:false scroll:false tab-size:3 lang:ps decode:true \">Diff-Objects $process.MainModule (get-process pwsh).MainModule -Depth 3 -Parent '$.MainModule' -NullAndBlankSame $true<\/pre>\n<h1>The Code<\/h1>\n<p>The code to for this utility <a href=\"https:\/\/github.com\/Phil-Factor\/PowerShell-Utility-Cmdlets\/blob\/main\/Diff-Objects\/Diff-Objects.ps1\">is on Github<\/a>.\u00a0 and I feel that the code will change over time, so it might be best to <a href=\"https:\/\/github.com\/Phil-Factor\/PowerShell-Utility-Cmdlets\/blob\/main\/Diff-Objects\/Diff-Objects.ps1\">get it from Github<\/a>, as it is always trickier to update a published article.<\/p>\n<p>The code now requires <strong><a href=\"https:\/\/github.com\/Phil-Factor\/PowerShell-Utility-Cmdlets\/blob\/main\/Display-Object\/Display-Object.ps1\">display-object<\/a><\/strong>. I just don\u2019t have the spare time to maintain both codebases separately as they had so much in common, and I think <strong><a href=\"https:\/\/github.com\/Phil-Factor\/PowerShell-Utility-Cmdlets\/blob\/main\/Display-Object\/Display-Object.ps1\">display-object<\/a><\/strong> could be useful to you anyway. \u00a0I started using<a href=\"https:\/\/github.com\/Phil-Factor\/PowerShell-Utility-Cmdlets\/blob\/main\/Diff-Objects\/Diff-Objects.ps1\"> <strong>Diff-Objects<\/strong><\/a> to examine differences in databases from various RDBMSs\u00a0 and\u00a0 it wasn\u2019t giving me missing objects in the target that had no attributes. I started to tweak it, and suddenly felt it was so much easier just using Display-object to do the task. Who cares if it is a bit slower?<\/p>\n<pre class=\"theme:powershell-ise font:consolas font-size:13 line-height:12 marking:false ranges:false nums-toggle:false wrap:true wrap-toggle:false popup:false scroll:false tab-size:3 lang:ps decode:true\">&lt;#\r\n\t.SYNOPSIS\r\n\t\tUsed to Compare two powershell objects\r\n\t\r\n\t.DESCRIPTION\r\n\t\tThis compares two powershell objects by determining their shared \r\n     keys or array sizes and comparing the values of each. It uses the \r\n     Display-Object cmdlet for the heavy lifting\r\n\t\r\n\t\r\n\t.PARAMETER Ref\r\n\t\tThe source object \r\n\t\r\n\t.PARAMETER diff\r\n\t\tThe target object \r\n\t\r\n\t.PARAMETER Avoid\r\n\t\ta list of any object you wish to avoid comparing\r\n\t\r\n\t.PARAMETER Parent\r\n\t\tOnly used for recursion\r\n\t\r\n\t.PARAMETER Depth\r\n\t\tThe depth to which you wish to recurse\r\n\t\r\n\t.PARAMETER NullAndBlankSame\r\n\t\tDo we regard null and Blank the same for the purpose of comparisons.\r\n\r\n\t.PARAMETER $ReportNodes\r\n\t\tDo you wish to report on nodes containing objects as well as values?\r\n\t\r\n\t.NOTES\r\n\t\tAdditional information about the function.\r\n\r\n#&gt;\r\nfunction Diff-Objects\r\n{\r\n\tparam\r\n\t(\r\n\t\t[Parameter(Mandatory = $true,\r\n\t\t\t\t   Position = 1)]\r\n\t\t[object]$Ref,\r\n\t\t[Parameter(Mandatory = $true,\r\n\t\t\t\t   Position = 2)]\r\n\t\t[object]$Diff,\r\n\t\t[Parameter(Mandatory = $false,\r\n\t\t\t\t   Position = 3)]\r\n\t\t[object[]]$Avoid = @('Metadata', '#comment'),\r\n\t\t[Parameter(Mandatory = $false,\r\n\t\t\t\t   Position = 4)]\r\n\t\t[string]$Parent = '$',\r\n\t\t[Parameter(Mandatory = $false,\r\n\t\t\t\t   Position = 5)]\r\n        [string]$NullAndBlankSame = $true,\r\n\t\t[Parameter(Mandatory = $false,\r\n\t\t\t\t   Position = 6)]\r\n\t\t[int]$ReportNodes = $true,\r\n\t\t[Parameter(Mandatory = $false,\r\n\t\t\t\t   Position = 7)]\r\n\t\t[int]$Depth =10\r\n\t)\r\n\t\r\n\t$Left = display-object $Ref -Avoid $Avoid -Parent $Parent -Depth $Depth -reportNodes $ReportNodes\r\n\t$right = display-object $Diff -Avoid $Avoid -Parent $Parent -depth $Depth -reportNodes $ReportNodes\r\n\t$Paths = $Left + $Right | Select path -Unique\r\n\t$Paths | foreach{\r\n\t\t$ThePath = $_.Path;\r\n\t\t$Lvalue = $Left | where { $_.Path -eq $ThePath } | Foreach{ $_.Value };\r\n\t\t$Rvalue = $Right | where { $_.Path -eq $ThePath } | Foreach{ $_.Value };\r\n\t\tif ($RValue -eq $Lvalue)\r\n\t\t{ $equality = '==' }\r\n        elseif ([string]::IsNullOrEmpty($Lvalue) -and \r\n               [string]::IsNullOrEmpty($rvalue) -and \r\n               $NullAndBlankSame)\r\n               {$equality = '=='}\r\n \r\n\t\telse\r\n\t\t{\r\n\t\t\t$equality = \"$(if ($lvalue -eq $null) { '-' }\r\n\t\t\t\telse { '&lt;' })$(if ($Rvalue -eq $null) { '-' }\r\n\t\t\t\telse { '&gt;' })\"\r\n\t\t}\r\n\t\t[pscustomobject]@{ 'Ref' = $ThePath; 'Source' = $Lvalue; 'Target' = $Rvalue; 'Match' = $Equality }\r\n\t\t\r\n\t}\r\n}<\/pre>\n<p>&nbsp;<\/p>\n<h1>Conclusion<\/h1>\n<p>Almost every PowerShell task that involves comparing objects seems to come up with another requirement. The built-in Compare-Objects cmdlet is a good start and can be persuaded to do a lot of tasks, but nothing beats a cmdlet with the source that one can alter to suit. I don\u2019t like to develop anything I don\u2019t immediately need for my work so I\u2019m happy to leave something that does the job. One day I may come up with, for example with the need for a cmdlet that lists EVERY key\/value pair in the target that doesn\u2019t appear in the source, rather than to stop at the level of the first difference. I may need something that will compares arrays where the order is not significant. No worries, because now I can just add it and test it!<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A custom Diff-Objects PowerShell cmdlet for deep-comparing complex objects with nested properties, arrays, and custom types &#8211; more thorough than the built-in Compare-Object, which compares objects by overall equality rather than traversing nested properties. Complete source code and usage examples.&hellip;<\/p>\n","protected":false},"author":154613,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[2],"tags":[],"coauthors":[6813],"class_list":["post-92359","post","type-post","status-publish","format-standard","hentry","category-blogs"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/92359","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/154613"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=92359"}],"version-history":[{"count":8,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/92359\/revisions"}],"predecessor-version":[{"id":92774,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/92359\/revisions\/92774"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=92359"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=92359"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=92359"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=92359"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}