1

team!

I have variable with type PSObject[] in my advanced function.

[Parameter( Mandatory = $false, Position = 0, HelpMessage = "PsObject data." )]
[PSobject[]] $data,
...

but sometimes my input $data with type [string[]] is transforming to [PSObject[]] and i catch error while im using object property.

im trying to validate it by script

[Parameter( Mandatory = $false, Position = 0, HelpMessage = "PsObject data." )]
[ValidateScript({ ( ( $_ -is [PSobject] ) -or ( $_ -is [PSobject[]] )  -or ( $_ -is [System.Object[]] ) ) })]
 $data,

but it has no effect, i see the $data with type [string[]], i`am continue to cath errors.

Whats wrong?

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Every object in PowerShell is (transparently) wrapped in a `PSObject`, so converting to `PSObject` has no real effect. What is it you're hoping to enforce? If the input object needs to have specific properties you might want to define a custom `class`. – Mathias R. Jessen Oct 08 '21 at 11:19

2 Answers2

3

Edit: based on the comments, it sounds like your real question is:

How can I validate that I'm able to attach new properties to the input objects with Add-Member?

For that, you need to exclude two kinds of input values:

  • Objects of a value type (numerical types, [datetime]'s, anything passed by value in .NET really)
  • Strings

(As mklement0's excellent answer shows, properties can be added to local copies of these types - but PowerShell cannot predictably "resurrect" them when passing values between adjecent commands in a pipeline, among other quirks)

You can validate that input objects do not fall in one of these buckets, like this:

[ValidateScript({$null -ne $_ -and $_.GetType().IsValueType -and $_ -isnot [string]})]
[psobject[]]$InputObject

PSObject is a generic wrapper type that PowerShell uses internally to keep track of extended properties and members attached to existing objects.

For this reason, any object can be converted to PSObject implicitly - in fact, PowerShell does so every time an object passes from one command to another across | in a pipeline statement - and it has no real effect in terms of enforcing specific input object traits.

If you want to ensure that an object has specific properties, the best option is to define a specific datatype with the class keyword:

class MyParameterType 
{
  [string]$Name
  [int]$Value
}

function Test-MyParameterType
{
  param(
    [MyParameterType[]]$InputObject
  )

  $InputObject |ForEach-Object {
    $_.GetType() # this will output `[MyParameterType]`
    $_.Name # now you can be sure this property exists
  }
}

You can now pass instances of the declared type to the function parameter:

$mpt = [MyParameterType]::new()
$mpt.Name = 'Name goes here'

Test-MyParameterType -InputObject $mpt

But PowerShell can also implicitly convert custom objects to the desired target type if they have matching properties:

$arg = [pscustomobject]@{
  Name = 'A name'
  Value = Get-Random
}

# This will return [PSCustomObject]
$arg.GetType() 

# But once we reach `$_.GetType()` inside the function, it will have been converted to a proper [MyParameterType]
Test-MyParameterType -InputObject $arg 

If you want to validate the existence of specific properties and potentially their value without explicit typing, you have to access the hidden psobject memberset of the object in the validation script - note that it'll validate one item at a time:

function Test-RequiredProperty
{
  param(
    [ValidateScript({ $_ -is [PSObject] -and ($prop = $_.psobject.Properties['RequiredProperty']) -and $null -ne $prop.Value })]
    [PSObject[]]$InputObject
  )
}

Now, if we pass an object with a RequiredProperty property that has some value, the validation succeeds:

$arg = [pscustomobject]@{
  RequiredProperty = "Some value"
}

# This will succeed
Test-RequiredProperty -InputObject $arg

# This will fail because the property value is $null
$arg.RequiredProperty = $null
Test-RequiredProperty -InputObject $arg

# This will fail because the property doesn't exist
$arg = [pscustomobject]@{ ADifferentPropertyName = "Some value" }
Test-RequiredProperty -InputObject $arg
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
  • @Alex Sorry, I must have misunderstood your question. You said "i catch error while im using object property." - but you don't want to test whether the input object has that specific property afterall? – Mathias R. Jessen Oct 08 '21 at 12:05
  • Thank you, for complete answer! Im my case i`am just want to know base type. Not to test properties types. – Alexey I. Kuzhel Oct 08 '21 at 12:05
  • Currently i`am do it by validate type in my code ```powershell if ( ( $Data -is [PSobject] ) -or ( $Data -is [PSobject[]] ) -or ( $Data -is [System.Object[]] ) ){ $ValidDataType = $true } Else { Add-ToLog -Message "Data should be of type [PSObject[]]! But type is [$( $data.gettype() )]. " -Display -Status "error" $ValidDataType = $false } ``` – Alexey I. Kuzhel Oct 08 '21 at 12:06
  • I want to make the same by using ValidateScript. – Alexey I. Kuzhel Oct 08 '21 at 12:08
  • But _why_? What's the point of validating the type if not to ensure that it has the correct members/properties that you expect to use? :) – Mathias R. Jessen Oct 08 '21 at 12:09
  • This function display data in color table format. I dont know all property types. It normal if they converted to string. – Alexey I. Kuzhel Oct 08 '21 at 12:11
  • Can you add the part of the function that fails (because `[string[]]` is accepted as `[psobject[]]`) [to your question](https://stackoverflow.com/posts/69494996/edit)? Strings have a `Length` property, so if you aren't accessing specific properties I don't understand what problem you're trying to solve – Mathias R. Jessen Oct 08 '21 at 12:13
  • for example: $DataCopy | Add-Member -MemberType NoteProperty -Name 'Num' -Value 0 – Alexey I. Kuzhel Oct 08 '21 at 12:14
  • $SerialData = [System.Management.Automation.PSSerializer]::Serialize($Data) $DataCopy = [System.Management.Automation.PSSerializer]::Deserialize($SerialData) – Alexey I. Kuzhel Oct 08 '21 at 12:15
  • All you need for that is: `[ValidateScript({-not $_.GetType().IsValueType -and $_ -isnot [string]})]` – Mathias R. Jessen Oct 08 '21 at 12:18
1

To complement Mathias R. Jessen's helpful answer:

While it is not advisable to decorate instances of .NET value types and strings with ETS (Extended Type System) properties via Add-Member, you can do it, using the following idiom.

# Works with any non-$null object.
# Note the use of -PassThru
# Add -Force to override an existing property.
$decoratedObject = $object | Add-Member -PassThru foo bar 

Note: The Add-Member call is short for: Add-Member -PassThru -NotePropertyName foo -NotePropertyValue bar; that is, a property named .foo with value 'bar' is added.

-PassThru is only strictly needed if $object is a string (type [string]).

To demonstrate:

$decoratedNumber = 42 | Add-Member -PassThru foo bar1 
$decoratedString = 'baz' | Add-Member -PassThru foo bar2
# Access the .foo property on each:
($decoratedNumber, $decoratedString).foo # -> 'bar1', 'bar2'

Why doing so isn't advisable:

The decorating properties:

  • are discarded when passing or casting decorated value-type instances to parameters of the same type or when modifying the variable's value:

    PS> ++$decoratedNumber; $decoratedNumber.foo + '!'
    !  # !! Property was discarded. 
    
    PS> & { param([int] $num) $num.foo + '!' } $decoratedNumber
    !  # !! Property was discarded. 
    
    # Only *untyped* parameters preserve the decorations:
    PS> & { param($num) $num.foo + '!' } $decoratedNumber
    bar1!
    
  • can unexpectedly surface in serialization contexts; e.g.:

    PS> $decoratedNumber | ConvertTo-Json -Compress
    {"value":42,"foo":"bar1"}
    
    # Casting to the original type helps:
    PS> [int] $decoratedNumber | ConvertTo-Json -Compress
    42
    

Additionally, for [int] instances specifically, values between 0 and 100 are internally cached by PowerShell (for performance reasons), so the decorations may unexpectedly surface in later, unrelated variables:

 PS> $decorated = 42 | Add-Member -PassThru foo bar1; $other = 42; $other.foo
 bar1 # !! Unrelated use of 42 is decorated too.

For more information, see this answer.

mklement0
  • 382,024
  • 64
  • 607
  • 775