Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Objects are situationally invisibly [psobject]-wrapped, sometimes causing unexpected behavior. #5579

Open
mklement0 opened this issue Nov 29, 2017 · 2 comments
Labels
Issue-Discussion the issue may not have a clear classification yet. The issue may generate an RFC or may be reclassif WG-Engine core PowerShell engine, interpreter, and runtime

Comments

@mklement0
Copy link
Contributor

mklement0 commented Nov 29, 2017

There are (at least?) 5 basic scenarios in which objects / properties end up invisibly [psobject]-wrapped, which can lead to subtle differences in behavior:

Note: By [psobject]-[extra-]wrapped I mean an object for which -is [psobject] returns $true.

  • Objects received via the pipeline when processed via $_ / $PSItem or an [object] or untyped parameter - but not if passed as an argument:
###  Via the *pipeline*:
# -> $true: use of $_
'foo' | ForEach-Object { $_ -is [psobject] }
# -> $true: use of [object]-typed parameter (or untyped)
'foo' | & { param([parameter(valuefrompipeline)] [object] $foo) process { $foo -is [psobject] } }

# -> $false: use of a specifically typed parameter
'foo' | & { param([parameter(valuefrompipeline)] [string] $foo) process { $foo -is [psobject] } }

### As an *argument*:
# -> $false: untyped parameter (or [object]-typed), or any type other than [psobject]
& { param($p) $p -is [psobject] } 'foo'
& { param([string] $p) $p -is [psobject] } 'foo'
# -> $true: only with explicit [psobject]-typing:
& { param([psobject] $p) $p -is [psobject] } 'foo'
  • Objects output by a - binary - cmdlet - but not objects output by a PowerShell function or script or objects returned from an expression. (Note that the arrays that PowerShell implicitly constructs for collecting multiple output objects on assignment to a variable or when a command call participates in an expression are themselves not [psobject]-wrapped.)
# -> $true: output from binary *cmdlet*
(Write-Output 'foo') -is [psobject]

# -> $false: output from *PowerShell code*
(& { 'foo' }) -is [psobject]

# -> $false: output from an expression
'foo' -is [psobject]
  • The value of any calculated property whose value is determined via a script block - but not if the value is determined by a property name (string):
# -> $true: property value is determined by *script block*
(Get-Item / | Select-Object @{ l='foo'; e={ $_.Name } }).foo -is [psobject]

# -> $false: property value is determined by *name* (string)
(Get-Item / | Select-Object @{ l='foo'; e='Name' }).foo -is [psobject]
  • The elements of the collections returned by the intrinsic .ForEach() and .Where() methods are always [psobject]-wrapped (implied by the output collection type, System.Collections.ObjectModel.Collection<PSObject>):
# -> $true, $true
(1..2).ForEach({ $_ }) | ForEach-Object { $_ -is [psobject] }
# -> $true
([psobject] 42) -is [psobject]

# -> $true: [pscustomobject] is the same as [psobject] and 
#   does *not* create a custom object from arbitrary operands
# (only [pscustomobject] @{ ... } works)
([pscustomobject] 42) -is [psobject]

As for real-world ramifications (in addition to the difference in Get-Member representation - see below):

Note that even non-wrapped instances in the sense above DO have ETS-defined properties (e.g., [datetime]::now.psextended reveals the .DateTime property).

(While #4347 may sound related, the distinct issue there is that additional properties may be added to outputs by provider cmdlets.)

Examples:

# Two seemingly equivalent ways of constructing a [string] instance:
# Using an expression vs. using a command:
$o1 = 'hi'; $o2 = New-Object System.String 'hi'

# Only the New-Object (*command*-generated) instance is wrapped, however.
> ($o1 -is [psobject]), ($o2 -is [psobject])
False
True

# With *multiple outputs*, *only the individual elements are wrapped*,
# because the implicitly constructed array that collects the output is itself
# NOT wrapped.
> $arr = Write-Output 1, 2; $arr -is [psobject]; $arr[0] -is [psobject]
False
True

# Two ways of constructing a [pscustomobject] instance
# with an array-valued .foo property that is *not* wrapped
# ($o2 itself, by contrast, *is* wrapped, because it is constructed with
# a *conmand*).
$o1 = [pscustomobject] @{ foo = 1, 2 }
$o2 = New-Object pscustomobject -property @{ 'foo' = 1, 2  }

# *Seemingly* equivalent ways, which, however result in a [psobject]-wrapped
# .foo property.
# Calculated property:
$o3 = '' | Select-Object @{ l='foo'; e = { 1, 2 } }
# Explicit [psobject] cast:
$o4 = [pscustomobject] @{ foo = [psobject] (1, 2) }

# All are instances of [System.Management.Automation.PSCustomObject]
> $o1, $o2, $o3, $o4 | % GetType | % Name
PSCustomObject
PSCustomObject
PSCustomObject
PSCustomObject

# All .foo properties are instances of [System.Object[]]
> $o1.foo, $o2.foo, $o3.foo, $o4.foo | % GetType | % Name
Object[]
Object[]
Object[]
Object[]

# However, only $o3 and $o4's .foo properties are considered [psobject] instances:
> ($o1.foo -is [psobject]), ($o2.foo -is [psobject]), ($o3.foo -is [psobject]), ($o4.foo -is [psobject])
False
False
True
True

# This subtle distinction can result in different behavior.
# Note how Get-Member represents the properties differently.
> $o1, $o3 | Get-Member foo | % Definition
Object[] foo=System.Object[]
System.Object[] foo=1 2

# *Windows PowerShell* only:
# Note how the JSONification of $o3 has an extraneous "value" wrapper for
# the array and a "Count" property.
> $o1, $o3 | ConvertTo-Json
[
  {
      "foo":  [
                  1,
                  2
              ]
  },
  {
      "foo":  {
                  "value":  [
                                1,
                                2
                            ],
                  "Count":  2
              }
  }
]

Environment data

Current as of:

PowerShell Core 7.1.0-preview.6
Windows PowerShell 5.1
@san7hos
Copy link

san7hos commented Aug 1, 2018

This is also observable in PS 5.1

> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      5.1.17134.165
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.17134.165
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
C:\WINDOWS\system32
> $x = @(1,@(2,3),4)
C:\WINDOWS\system32
> $x | ConvertTo-Json -Compress
[1,{"value":[2,3],"Count":2},4]

@mklement0
Copy link
Contributor Author

@san7hos: That's actually a slightly different problem that only affects Windows PowerShell at this point: the problem is caused by an ETS .Count property attached to [System.Array] - see this SO answer for details.

In your example, the nested array does not have a [psobject] wrapper - $x[1] -is [psobject] yields $False - but it still triggers the serialization bug, because ConvertTo-Json sees this array as a single object.

In my example above, the [psobject] wrapper makes the difference: only if array-valued property .foo contains an extra-[psobject]-wrapped array does ConvertTo-Json apply the ETS data; here's a succinct recap:

# .foo as a regular array: works as expected in both PS Core and Windows PowerShell
PS> [pscustomobject] @{ foo = 0, 0 } | ConvertTo-Json -Compress
{"foo":[0,0]}   # OK

# .foo being extra-[psobject]-wrapped: serialized via ETS definition for [System.Array],
# which only exists in *Windows PowerShell* at this point, 
# so you'll only see the symptom there.
PS> [pscustomobject] @{ foo = New-Object int[] 2 } | ConvertTo-Json -Compress
{"foo":{"value":[0,0],"Count":2}}   # !! Extraneous .Count, .value properties

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Issue-Discussion the issue may not have a clear classification yet. The issue may generate an RFC or may be reclassif WG-Engine core PowerShell engine, interpreter, and runtime
Projects
None yet
Development

No branches or pull requests

3 participants