Search code examples
powershellreflection

Powershell custom object hidden property types


Background:

I have a PSCustomObject that was created by converting a JSON array via ... | convertfrom-json. The object has a lot of other objects for the property values (basically it's a collection of a lot of PSCustomObjects). From knowing the object I know it contains at least three different types of objects (types meaning PSCustomObject with different properties).

Issue:

When running Get-Member I only two object types and their members, the third one is not listed at all. I know there is a third object type as I can select properties that are only available in that object.

Note:

I did have once a similar issue, where some members would only appear in the results of get-member only if called first in a $object | select... method, otherwise they just didn't show up. I didn't figure it out then either. The current issue is not the same but might be related, as I tried the method of $object | select... and it didn't help.

Note2:

I did notice when trying to post code that is reproducible I get only one object type in return instead of two I get from the invoke-restmethod, this makes my question even bigger, what's going on here, why are some object types returned and some not.

Example:

Example of get-member result

$res.address_objects.ipv4 | gm       


   TypeName: Selected.System.Management.Automation.PSCustomObject
Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
host        NoteProperty System.Management.Automation.PSCustomObject <snip>
name        NoteProperty string name=<snip>
uuid        NoteProperty string uuid=<snip>
zone        NoteProperty string zone=<snip>

   TypeName: System.Management.Automation.PSCustomObject
Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
name        NoteProperty string name=<snip>
network     NoteProperty System.Management.Automation.PSCustomObject <snip>
uuid        NoteProperty string uuid=<snip>

As you can see there are two object types here and they both have some different property names.

Sample:

Sample Json that I convert to an object.

Taken from @Jawad's answer.

Please note: This sample is not an exact copy of my code as my psobject is the result of a invoke-restmethod that automatically converts the json to an object.

$json = @"
{
  "address_objects": {
    "ipv4": [{
        "host": "hostValue",
        "name": "hostName",
        "uuid": "value",
        "zone": "thisZone"
     },
     {
        "name": "NewName",
        "network": "newNetwork",
        "uuid": "thisUuid"
     },
     {
        "name": "NewName",
        "range": "newrange",
        "uuid": "thisUuid"
     }]
  }
}
"@ | ConvertFrom-Json

Output I expected When running Get-member:

$json.address_objects.ipv4 | gm       


   TypeName: Selected.System.Management.Automation.PSCustomObject
Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
host        NoteProperty System.Management.Automation.PSCustomObject <snip>
name        NoteProperty string name=<snip>
uuid        NoteProperty string uuid=<snip>
zone        NoteProperty string zone=<snip>

   TypeName: System.Management.Automation.PSCustomObject
Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
name        NoteProperty string name=<snip>
network     NoteProperty System.Management.Automation.PSCustomObject <snip>
uuid        NoteProperty string uuid=<snip>
   

   TypeName: System.Management.Automation.PSCustomObject
Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
name        NoteProperty string name=<snip>
range     NoteProperty System.Management.Automation.PSCustomObject <snip>
uuid        NoteProperty string uuid=<snip>

Basically there is three distinct psCustomObjects so get-member should list them all three.

Edit #1:

Edited thanks to the commenter, they were right so I added a reproducible sample and clarified what I'm asking about. I haven't yet dissected in-depth the answers given.


Solution

  • Get-Member by design lists the distinct types among its input objects.[1]

    However, the problem with [pscustomobject] instances is that Get-Member does not recognize them as different types even if they have differing properties.

    # Send 3 [pscustomobject] instances with distinct properties to Get-Member
    [pscustomobject] @{ one = 1; two = 2; three = 3 },
    [pscustomobject] @{ four = 4; five = 5 },
    [pscustomobject] @{ six = 6; seven = 7 } | Get-Member
    

    The following unexpectedly yields only a single output object, showing only the first [pscustomobject] instance's members:

       TypeName: System.Management.Automation.PSCustomObject
    
    Name        MemberType   Definition
    ----        ----------   ----------
    Equals      Method       bool Equals(System.Object obj)
    GetHashCode Method       int GetHashCode()
    GetType     Method       type GetType()
    ToString    Method       string ToString()
    one         NoteProperty int one=1
    three       NoteProperty int three=3
    two         NoteProperty int two=2
    

    Get-Member distinguishes types only by their (full) type names, as reflected in the hidden instance property .pstypenames's first element (.pstypenames[0]), without considering a given instance's specific properties.
    That type name for [pscustomobject] instances is System.Management.Automation.PSCustomObject by default.

    Note that .pstypenames[0] by default contains the same type name as .GetType().FullName, but "made-up" names may be inserted[2], which is what happens with [pscustomobject] instances created by the Select-Object cmdlet, for instance (see bottom section).


    Workaround:

    Note: The following works for display output (which should be fine, given that Get-Member output is usually used for visual inspection).

    [pscustomobject] @{ one = 1; two = 2; three = 3 },
    [pscustomobject] @{ four = 4; five = 5 },
    [pscustomobject] @{ six = 6; seven = 7 } |
      Group-Object { "$($_.psobject.Properties.Name)" } | ForEach-Object {
        Get-Member -InputObject $_.Group[0] | Out-Host
      }
    
    • Group-Object is used to group the input objects by their list of property names, using a calculated property (via a script block ({ ... }) that is evaluated for each input object).

      • $_.psobject.Properties.Name yields an array of all property names, and "$(...)" converts that into a space-separated list.
    • Each group is then processed via ForEach-Object, passing each group's first instance ($_.Group[0]) directly to Get-Member

      • So as to ensure that individual Get-Member calls produce individual display output, Out-Host is used; without it, the display output would mistakenly suggest a single input type comprising the properties across all distinct types.

    If you're only interested in the list of distinct property names, across all input objects:

    # This yields the sorted array of all unique property names, across all
    # input objects:
    #     'five', 'four', 'one', 'seven', 'six', 'three', 'two'
    [pscustomobject] @{ one = 1; two = 2; three = 3 },
    [pscustomobject] @{ four = 4; five = 5 },
    [pscustomobject] @{ six = 6; seven = 7 } |
      ForEach-Object { $_.psobject.Properties.Name } | Sort-Object -Unique
    

    As for your symptoms:

    Note that your first Get-Member output block mentions a different type name: Selected.System.Management.Automation.PSCustomObject

    The Selected. prefix implies that the object was created via the Select-Object cmdlet. While such an object is technically also a [pscustomobject] instance, the modified type name causes Get-Member to treat it as a different type.

    Here's a simplified example:

    $obj = [pscustomobject] @{ one = 1; two = 2 }
    $obj, ($obj | Select-Object -Property *) | Get-Member
    

    This yields the following; note how the properties are the same and only the type name differs:

       TypeName: System.Management.Automation.PSCustomObject
    
    Name        MemberType   Definition
    ----        ----------   ----------
    Equals      Method       bool Equals(System.Object obj)
    GetHashCode Method       int GetHashCode()
    GetType     Method       type GetType()
    ToString    Method       string ToString()
    one         NoteProperty int one=1
    two         NoteProperty int two=2
    
       TypeName: Selected.System.Management.Automation.PSCustomObject
    
    Name        MemberType   Definition
    ----        ----------   ----------
    Equals      Method       bool Equals(System.Object obj)
    GetHashCode Method       int GetHashCode()
    GetType     Method       type GetType()
    ToString    Method       string ToString()
    one         NoteProperty int one=1
    two         NoteProperty int two=2
    

    However, note that just like all [pscustomobject] instances with type name System.Management.Automation.PSCustomObject are treated the same even with differing properties, so are all the ones with Selected.System.Management.Automation.PSCustomObject. That is, Select-Object-created [pscustomobject] instances are also all treated the same, due to sharing the same, fixed type name.


    [1] For instance, 1, 2, 3 | Get-Member lists only one type, System.Int32, because all input objects have that type; by contrast, 1, 'foo', 2 | Get-Member lists two types, System.Int32 and System.String (but not System.Int32 again).

    [2] The ability to assign arbitrary type names is part of PowerShell's ETS (Extended Type System) - see about_types.ps1xml