Repository navigation
Description
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]-wrapped I mean an object for which -is [psobject] returns $true.
- Objects received via the pipeline when processed via
$_/$PSItemor 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] }- Perhaps not surprisingly, if you cast / type-constrain with
[psobject]- for which there is no need, and in most scenarios this will have no practical implications; more surprisingly, however, the same applies to[pscustomobject], given that these type accelerators are effectively the same and both refer to[System.Management.Automation.PSObject](see Why is [pscustomobject] the same as [psobject], even though a distinct [System.Management.Automation.PSCustomObject] type exists? #4344).
# -> $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):
-
In Windows PowerShell,
ConvertTo-Jsonis affected, as originally reported
in ConvertFrom-Json doesn't unwrap arrays on output, resulting in an extraneous wrapper JSON object withCountandValueproperties on reconversion #3153 - see below.
(PowerShell Core is no longer affected, because that issue has been fixed
via Remove System.Array type data #3231) -
The -f operator doesn't recognize a [psobject]-wrapped array #14355
-
A "hybrid" bareword command argument that is parsed as a number unexpectedly behaves like a string with -f (format operator) #17199 [declared to be by design]
-
Array.Sort method call situationally ignores string-comparer argument #14829
-
Invoke-Command -SSHConnection breaks if the HostName string is [psobject]-wrapped #10687 [fixed in v7.0.0-preview.5]
-
Conversion of [datetime] property in class does not work #20401 (comment)
-
Write-Host renders
IDictionaryinconsistently depending on if the value is piped in #24671 -
Start-Transcriptthrows when$Transcriptis aPSObjectwrappedstring#24740 -
Binary cmdlet binding fails to unwrap a scalar PSObject for StringCollection parameters #27969
-
ConvertTo-Json does not convert additional members of nested properties #19185 (comment)
-
$_in aForEach-Objectscript block reflects each input object[psobject]-wrapped, causing-is [pscustomobject]tests to become meaningless, as then any input object passes the test - see this SO question -
Passing an incidentally wrapped
[psobject]-wrapped instance to an external serialization API such as[Newtonsoft.Json.JsonConvert]::SerializeObject()causes unexpected behavior - see this Stack Overflow post. -
For the sake of completeness: bugs that still affect Windows PowerShell, but have since been fixed:
-
An
[xml]-related bug: see this Stack Overflow post. -
The
$Transcriptpreference variable doesn't recognize a[psobject]-wrapped string (such as output byJoin-Path) as a string: see this Stack Overflow post.
-
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.5.0-rc.1
Windows PowerShell 5.1
Activity
san7hos commentedon Aug 1, 2018
This is also observable in PS 5.1
mklement0 commentedon Aug 1, 2018
Michal Stasa (san7hos) (@san7hos): That's actually a slightly different problem that only affects Windows PowerShell at this point: the problem is caused by an ETS
.Countproperty 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, becauseConvertTo-Jsonsees this array as a single object.In my example above, the
[psobject]wrapper makes the difference: only if array-valued property.foocontains an extra-[psobject]-wrapped array doesConvertTo-Jsonapply the ETS data; here's a succinct recap:microsoft-github-policy-service commentedon Jun 21, 2026
This issue has not had any activity in 6 months, if there is no further activity in 7 days, the issue will be closed automatically.
Activity in this case refers only to comments on the issue. If the issue is closed and you are the author, you can re-open the issue using the button below. Please add more information to be considered during retriage. If you are not the author but the issue is impacting you after it has been closed, please submit a new issue with updated details and a link to this issue and the original.