Powershell: Non-deterministic behavior with `$value -is [psobject]`

Created on 8 May 2019  路  10Comments  路  Source: PowerShell/PowerShell

Steps to reproduce

I am trying to recurse parsed JSON, but value types are passing type checks for [psobject] and [pscustomobject] making the recursive case impossible to detect.

function repro($v) {
    $v.GetType()
    $v -is [psobject]
}
repro ([int64]4)
repro ("4" | ConvertFrom-Json)

Expected behavior

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int64                                    System.ValueType
False
True     True     Int64                                    System.ValueType
False

Actual behavior

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int64                                    System.ValueType
False
True     True     Int64                                    System.ValueType
True

Environment data

Name                           Value
----                           -----
PSVersion                      6.2.0
PSEdition                      Core
GitCommitId                    6.2.0
OS                             Darwin 18.5.0 Darwin Kernel Version 18.5.0: Mon Mar 11 20:40:32 PDT 2019; root:xnu-4903.251.3~3/RELEASE_X86_64
Platform                       Unix
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0鈥
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Also observed on Windows

Issue-Question

All 10 comments

Yeah. _Every_ type you can find in PowerShell is wrapped in a [psobject], by design. PSObject is a wrapper type that enables the Extended Type System functions used pretty extensively in PS.

For that matter, -is [PSObject] and -is [PSCustomObject] are identical; if you check either type's fullname you'll see they point to the same class. The difference there is how the parser handles the [pscustomobject] version differently, specifically in the [pscustomobject]@{} situation where it's followed by a hashtable.

If you need to find a PSCustomObject type specifically, you'll need to do your checks differently.

function Test-PSCustomObject {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [psobject]
        $InputObject
    )
    process {
        $InputObject.PSTypeNames -contains 'System.Management.Automation.PSCustomObject'
    }
}

Test-PSCustomObject ([int64]4) # gives False
Test-PSCustomObject ("4" | ConvertFrom-Json) # gives False
Test-PSCustomObject ('{"property":"value"}'| ConvertFrom-Json) # gives True

For my use case, I ended up using -isnot [ValueType]. It's pretty common for interpreted languages to have wrapper types and I'm OK with that. In that case, I would expect ([int64]4) -is [psobject] to be $true. My bug is that the wrapper type is inconsistently applied (as evident with -is [psobject]) and not clearly exposed to the user (.GetType() outputs are identical)

Can you check again with 6.2? I checked that case briefly and I'm pretty sure it was giving back what you expect there. I'll check again, just to be sure.

Also, I don't think [string] will register as a value type, from memory, so mind that. :)

I just updated to 6.2 and I'm still observing the same output. Also confirmed you are right about -isnot [ValueType]

Hmm, you are correct. @SteveL-MSFT is there a provision for basic value types where they aren't usually wrapped in PSObject wrappers for some reason?

@chriskuech I'd probably recommend using a switch in this instance for processing the items in your json, something along the lines of:

switch ($Item) {
    {$_ -is [string]} {
        # process $_ as string
    }
    {$_ -is [ValueType]} {
        # process $_ as value type
    }
    default {
        # process $_ as object / custom object (JSON only generates custom objects anyway)
    }
}

Thanks, I did not know scriptblocks are acceptable switch cases. In this case I have to use ifs because it's a merge function on two objects, not just a traverse function on a single object.

Yeah, switch does some fun things. More on that from Kevin Marquette's lovely comprehensive blog post here: https://powershellexplained.com/2018-01-12-Powershell-switch-statement/

If you need more help with this, feel free to jump into the PS Discord / Slack channels as well. 馃槃

@vexx32 unfortunately, I'm not familiar with the history of this

My bug is that the wrapper type is inconsistently applied

Indeed - see #5579 and #4343.

However, -is [System.Management.Automation.PSCustomObject] works consistently:

([int64] 4), 
("4" | ConvertFrom-Json), 
([pscustomobject] @{ val = 4 }), 
([pscustomobject] @{ val = 4 } | Select-Object val) | 
  & {
       param([Parameter(ValueFromPipeline)] $obj) 

      process { 
         $obj -is [System.Management.Automation.PSCustomObject]
      } 
  }

-> $false, $false, $true, $true

Was this page helpful?
0 / 5 - 0 ratings