How do I preserve Enums as 'Strings' in ConvertTo-Json cmdlet? [duplicate] - powershell

For "Get-Msoldomain" powershell command-let I get the below output (lets call it Output#1) where Name, Status and Authentication are the property names and below are their respective values.
Name Status Authentication
myemail.onmicrosoft.com Verified Managed
When I use the command with "ConvertTo-Json" like below
GetMsolDomain |ConvertTo-Json
I get the below output (lets call it Output#2) in Json Format.
{
"ExtensionData": {
},
"Authentication": 0,
"Capabilities": 5,
"IsDefault": true,
"IsInitial": true,
"Name": "myemail.onmicrosoft.com",
"RootDomain": null,
"Status": 1,
"VerificationMethod": 1
}
However, the problem is, that if you notice the Status property in both the outputs, it's different. Same happens for VerificationMethod property. Without using the ConvertTo-JSon Powershell gives the Text, and with using ConvertTo-Json it gives the integer.
When I give the below command
get-msoldomain |Select-object #{Name='Status';Expression={"$($_.Status)"}}|ConvertTo-json
I get the output as
{
"Status": "Verified"
}
However, I want something so that I don't have to specify any specific property name for it to be converted , the way I am specifying above as
Select-object #{Name='Status';Expression={"$($_.Status)"}}
This line is transforming only the Status Property and not the VerificationMethod property because that is what I am providing as input .
Question: Is there something generic that I can give to the "ConvertTo-Json" commandlet, so that It returns ALL the Enum properties as Texts and not Integers, without explicitly naming them, so that I get something like below as the output:
{
"ExtensionData": {
},
"Authentication": 0,
"Capabilities": 5,
"IsDefault": true,
"IsInitial": true,
"Name": "myemail.onmicrosoft.com",
"RootDomain": null,
"Status": "Verified",
"VerificationMethod": "DnsRecord"
}

Well, if you don't mind to take a little trip :) you can convert it to CSV which will force the string output, then re-convert it back from CSV to PS Object, then finally back to Json.
Like this:
Get-MsolDomain | ConvertTo-Csv | ConvertFrom-Csv | ConvertTo-Json
If you need to keep the original Types instead of converting it all to string see mklement0 helpful answer...

PowerShell Core (PowerShell versions 6 and above) offers a simple solution via ConvertTo-Json's -EnumsAsStrings switch.
GetMsolDomain | ConvertTo-Json -EnumsAsStrings # PS *Core* (v6+) only
Unfortunately, this switch isn't supported in Windows PowerShell.
Avshalom's answer provides a quick workaround that comes with a big caveat, however: All property values are invariably converted to strings in the process, which is generally undesirable (e.g., the Authentication property's numeric value of 0 would turn into string '0').
Here's a more generic workaround based on a filter function that recursively introspects the input objects and outputs ordered hashtables that reflect the input properties with enumeration values converted to strings and all other values passed through, which you can then pass to ConvertTo-Json:
Filter ConvertTo-EnumsAsStrings ([int] $Depth = 2, [int] $CurrDepth = 0) {
if ($_ -is [enum]) { # enum value -> convert to symbolic name as string
$_.ToString()
} elseif ($null -eq $_ -or $_.GetType().IsPrimitive -or $_ -is [string] -or $_ -is [decimal] -or $_ -is [datetime] -or $_ -is [datetimeoffset]) {
$_
} elseif ($_ -is [Collections.IEnumerable] -and $_ -isnot [Collections.IDictionary]) { # enumerable (other than a dictionary)
, ($_ | ConvertTo-EnumsAsStrings -Depth $Depth -CurrDepth ($CurrDepth+1))
} else { # non-primitive type or dictionary (hashtable) -> recurse on properties / entries
if ($CurrDepth -gt $Depth) { # depth exceeded -> return .ToString() representation
Write-Warning "Recursion depth $Depth exceeded - reverting to .ToString() representations."
"$_"
} else {
$oht = [ordered] #{}
foreach ($prop in $(if ($_ -is [Collections.IDictionary]) { $_.GetEnumerator() } else { $_.psobject.properties })) {
if ($prop.Value -is [Collections.IEnumerable] -and $prop.Value -isnot [Collections.IDictionary] -and $prop.Value -isnot [string]) {
$oht[$prop.Name] = #($prop.Value | ConvertTo-EnumsAsStrings -Depth $Depth -CurrDepth ($CurrDepth+1))
} else {
$oht[$prop.Name] = $prop.Value | ConvertTo-EnumsAsStrings -Depth $Depth -CurrDepth ($CurrDepth+1)
}
}
$oht
}
}
}
Caveat: As with ConvertTo-Json, the recursion depth (-Depth) is limited to 2 by default, to prevent infinite recursion / excessively large output (as you would get with types such as [System.IO.FileInfo] via Get-ChildItem, for instance). Similarly, values that exceed the implied or specified depth are represented by their .ToString() value. Use -Depth explicitly to control the recursion depth.
Example call:
PS> [pscustomobject] #{ p1 = [platformId]::Unix; p2 = 'hi'; p3 = 1; p4 = $true } |
ConvertTo-EnumsAsStrings -Depth 2 |
ConvertTo-Json
{
"p1": "Unix", # Enum value [platformId]::Unix represented as string.
"p2": "hi", # Other types of values were left as-is.
"p3": 1,
"p4": true
}
Note: -Depth 2 isn't necessary here, given that 2 is the default value (and given that the input has depth 0), but it is shown here as a reminder that you may want to control it explicitly.
If you want to implement custom representations for additional types, such as [datetime], [datetimoffset] (using the ISO 8601-compatible .NET round-trip date-time string format, o, as PowerShell (Core) v6+ automatically does), as well as [timespan], [version], [guid] and [ipaddress], see Brett's helpful variation of this answer.

I needed to serialize pwsh objects to JSON, and was not able to use the -EnumsAsStrings parameter of ConvertTo-Json, as my code is running on psv5. As I encountered infinite loops while using #mklement0's code Editor's note: since fixed., I rewrote it. My revised code also deals with the serialization of some other types such as dates, serializing them into the ISO 8601 format, which is generally the accepted way to represent dates in JSON. Feel free to use this, and let me know if you encounter any issues.
Filter ConvertTo-EnumsAsStrings ([int] $Depth = 10, [int] $CurrDepth = 0) {
if ($CurrDepth -gt $Depth) {
Write-Error "Recursion exceeded depth limit of $Depth"
return $null
}
Switch ($_) {
{ $_ -is [enum] -or $_ -is [version] -or $_ -is [IPAddress] -or $_ -is [Guid] } {
$_.ToString()
}
{ $_ -is [datetimeoffset] } {
$_.UtcDateTime.ToString('o')
}
{ $_ -is [datetime] } {
$_.ToUniversalTime().ToString('o')
}
{ $_ -is [timespan] } {
$_.TotalSeconds
}
{ $null -eq $_ -or $_.GetType().IsPrimitive -or $_ -is [string] -or $_ -is [decimal] } {
$_
}
{ $_ -is [hashtable] } {
$ht = [ordered]#{}
$_.GetEnumerator() | ForEach-Object {
$ht[$_.Key] = ($_.Value | ConvertTo-EnumsAsStrings -Depth $Depth -CurrDepth ($CurrDepth + 1))
}
if ($ht.Keys.Count) {
$ht
}
}
{ $_ -is [pscustomobject] } {
$ht = [ordered]#{}
$_.PSObject.Properties | ForEach-Object {
if ($_.MemberType -eq 'NoteProperty') {
Switch ($_) {
{ $_.Value -is [array] -and $_.Value.Count -eq 0 } {
$ht[$_.Name] = #()
}
{ $_.Value -is [hashtable] -and $_.Value.Keys.Count -eq 0 } {
$ht[$_.Name] = #{}
}
Default {
$ht[$_.Name] = ($_.Value | ConvertTo-EnumsAsStrings -Depth $Depth -CurrDepth ($CurrDepth + 1))
}
}
}
}
if ($ht.Keys.Count) {
$ht
}
}
Default {
Write-Error "Type not supported: $($_.GetType().ToString())"
}
}
}

Related

Powershell - Exchange JSON output without needing to write to a file

EDIT: Added Setupconfigfiles.ps1
I'm a bit new to detailed scripting so please bear with me.
I have two Powershell scripts
Setupconfigfiles.ps1 generates JSON output to be fed to an API.
Script2 uses that JSON data to execute API commands.
Script 2 can call setupconfigfiles.ps1 as indicated below and use the output data.
.\SetupConfigFiles.ps1 -type $Type -outfile .\Templist.json
$servers = Get-Content -Raw -Path .\templist.json | ConvertFrom-Json
setupconfigfiles.ps1:
param (
# If this parameter is set, format the output as csv.
# If this parameter is not set, just return the output so that the calling program can use the info
[string]$outfile,
# this parameter can be 'production', 'development' or 'all'
[string]$type
)
enum MachineTypes {
production = 1
development = 2
all = 3
}
$Servers = Get-ADObject -Filter 'ObjectClass -eq "computer"' -SearchBase 'Obfuscated DSN' | Select-Object Name
$output = #()
$count = 0
# Set this to [MachineTypes]::production or [MachineTypes]::development or [MachineTypes]::all
if ($type -eq "all") {
$server_types = [MachineTypes]::all
}
ElseIf ($type -eq "production") {
$server_types = [MachineTypes]::production
}
else {
$server_types = [MachineTypes]::development
}
ForEach ($Server in $Servers)
{
$count = $count + 1
$this_server = #{}
$this_server.hostname = $Server.Name
$this_server.id = $count
$this_server."site code" = $this_server.hostname.substring(1,3)
$this_server."location code" = $this_server.hostname.substring(4,2)
if ($this_server.hostname.substring(7,1) -eq "P") {
$this_server.environment = "Production"
}
ElseIf ($this_server.hostname.substring(7,1) -eq "D") {
$this_server.environment = "Development"
}
Else {
$this_server.environment = "Unknown"
}
if (($server_types -eq [MachineTypes]::production ) -and ($this_server.environment -eq "Production")) {
$output += $this_server
}
ElseIf (($server_types -eq [MachineTypes]::development ) -and ($this_server.environment -eq "Development")) {
$output += $this_server
}
Else {
if ($server_types -eq [MachineTypes]::all ) {
$output += $this_server
}
}
}
if ($outfile -eq "")
{
ConvertTo-Json $output
}
else {
ConvertTo-Json $output | Out-File $outfile
}
How can I do it without needing to write to the Templist.json file?
I've called this many different ways. The one I thought would work is .\SetupConfigFiles.ps1 $servers
Y'all are great. #Zett42 pointed me in a direction and #Mathias rounded it out.
The solution was to change:
"ConvertTo-Json $output" to "Write-Output $output"
Then it's handled in the calling script.
thanks!

How can I check if a 2 numbers have the same digits PowerShell?

I want to compare two int so if they contain the same digits it outputs a true,for example:
$a=1260
$b=2106
and then because both of them contain: 0126 it outputs true how can this be made?
And if it's possible with the fewest possible lines
Here's one technique:
$null -eq (Compare-Object -ReferenceObject ([char[]][String]1260) -DifferenceObject ([char[]][String]2601))
Which returns true or false, depending on if the digits are the same or not.
Here is another, a bit lengthier solution:
( $a.ToString().ToCharArray() | ForEach-Object { $c = $true } { if ( $b.ToString() -notmatch $_.ToString() ) { $c = $false } } { $c } ) -and ( $b.ToString().ToCharArray() | ForEach-Object { $c = $true } { if ( $a.ToString() -notmatch $_.ToString() ) { $c = $false } } { $c } )
This compares the int's as arrays and thus need to be run bothways.

Powershell Convert a string to datatable

I have the below string which is returned from an Invoke-RestMethod call. How can I transform the results into an object or data table to be able to use it? The first row would be the properties.
I tried the Out-File and Export-CSV first but the only length is added to the file.
Then I tried the format table which did nothing.
Any ideas?
Name,Main No, Sec No
Alex,34.6743,22.7800
Tom,33.8798,21.9098
Tim,34.6743,41.7800
Mark,33.8798,21.9098
The ConvertFrom-Csv cmdlet is smart enough to exclude the empty lines we see in your example.
To output as CSV, simply use:
$data | ConvertFrom-Csv
#output to csv file:
$data | ConvertFrom-Csv | Export-Csv -Path 'D:\test.csv' -NoTypeInformation
Your CSV file will look like this:
"Name","Main No","Sec No"
"Alex","34.6743","22.7800"
"Tom","33.8798","21.9098 "
"Tim","34.6743","41.7800"
"Mark","33.8798","21.9098"
If you want it converted to datatable, you can use below function:
function ConvertTo-DataTable {
[CmdletBinding()]
Param(
[Parameter(Position=0, Mandatory=$true, ValueFromPipeline = $true)]
[PSObject[]]$InputObject
)
Begin {
$dataTable = New-Object System.Data.DataTable
$first = $true
function _GetSafeTypeName($type) {
# internal helper function to return the correct typename for a datatable
$types = #('System.Boolean', 'System.Byte', 'System.SByte', 'System.Char', 'System.Datetime',
'System.TimeSpan', 'System.Decimal', 'System.Double', 'System.Guid', 'System.Single')
$ints = #('System.Int16', 'System.Int32', 'System.Int64')
$uints = #('System.UInt16', 'System.UInt32', 'System.UInt64')
if ($types -contains $type) { return "$type" }
# if the type is Int or UInt, always return the largest variety
if ($ints -contains $type) { return 'System.Int64' }
if ($uints -contains $type) { return 'System.UInt64' }
return 'System.String'
}
}
Process {
foreach ($object in $InputObject) {
$dataRow = $dataTable.NewRow()
foreach($property in $object.PSObject.Properties) {
# read the data type for this property and make sure it is a valid type for a DataTable
$dataType = _GetSafeTypeName $property.TypeNameOfValue
# ensure the property name does not contain invalid characters
$propertyName = $property.Name -replace '[\W\p{Pc}-[,]]', '_' -replace '_+', '_'
if ($first) {
$dataColumn = New-Object System.Data.DataColumn $propertyName, $dataType
$dataTable.Columns.Add($dataColumn)
}
if ($property.Gettype().IsArray -or ($property.TypeNameOfValue -like '*collection*')) {
$dataRow.Item($propertyName) = $property.Value | ConvertTo-XML -As String -NoTypeInformation -Depth 1
}
else {
$value = if ($null -ne $property.Value) { $property.Value } else { [System.DBNull]::Value }
$dataRow.Item($propertyName) = $value -as $dataType
}
}
$dataTable.Rows.Add($dataRow)
$first = $false
}
}
End {
Write-Output #(,($dataTable))
}
}
Then use it like this:
$data | ConvertFrom-Csv | ConvertTo-DataTable
P.S. $data is the result of your Invoke-RestMethod call.

PowerShell Is there a way to get proper order of properties in Select-Object so that each object has order?

Following code lists all AD users.
$Domain.DomainUsersFullList = Get-ADUser -Server $Domain -ResultPageSize 500000 -Filter * -Properties *, "msDS-UserPasswordExpiryTimeComputed" | Select * -ExcludeProperty *Certificate, PropertyNames, *Properties, PropertyCount, Certificates
$($Domain.DomainUsersFullList[0]).PSObject.Properties.Name[12]
$($Domain.DomainUsersFullList[10]).PSObject.Properties.Name[12]
It seems that order returned by PSObject.Properties.Name can be different. Is there a way to order properties without exclusively telling Select-Object the order you want them in?
Just for the sake of why I need this:
https://github.com/EvotecIT/PSWriteWord - I wrote a function Add-WordTable that takes any $Object and puts this into Word document. No need to parse objects yourself. Just pass it to function and it will be put into Word document.
I am now working on same thing for:
https://github.com/EvotecIT/PSWriteExcel - which has function Add-ExcelWorksheetData that does exactly the same as above with one exception .. since it's Excel you don't have column limit. So with 100 columns I was/am getting wrong order per each row. Which makes no sense.
While in case of WORD document I didn't notice this issue because I never added more then 10 columns, with Excel and 100 columns I was getting wrong order. Below is an example of this:
Here is the method that does the conversion:
function Format-PSTableConvertType2 {
[CmdletBinding()]
param(
$Object,
[switch] $SkipTitles,
[string[]] $ExcludeProperty,
[switch] $NoAliasOrScriptProperties,
[switch] $DisplayPropertySet
)
[int] $Run = 0
$Array = New-ArrayList
$Titles = New-ArrayList
if ($NoAliasOrScriptProperties) {$PropertyType = 'AliasProperty', 'ScriptProperty' } else {$PropertyType = ''}
Write-Verbose "Format-PSTableConvertType2 - Option 2 - NoAliasOrScriptProperties: $NoAliasOrScriptProperties"
foreach ($O in $Object) {
$ArrayValues = New-ArrayList
if ($DisplayPropertySet -and $O.psStandardmembers.DefaultDisplayPropertySet.ReferencedPropertyNames) {
$ObjectProperties = $O.psStandardmembers.DefaultDisplayPropertySet.ReferencedPropertyNames.Where( { $ExcludeProperty -notcontains $_ } ) #.Name
} else {
$ObjectProperties = $O.PSObject.Properties.Where( { $PropertyType -notcontains $_.MemberType -and $ExcludeProperty -notcontains $_.Name } ).Name
}
#$ObjectProperties = $O.PSObject.Properties
foreach ($Name in $ObjectProperties) {
if ($Run -eq 0 -and -not $SkipTitle) { Add-ToArray -List $Titles -Element $Name }
Add-ToArray -List $ArrayValues -Element $O.$Name
}
if ($Run -eq 0 -and -not $SkipTitle) {Add-ToArray -List ($Array) -Element $Titles }
Add-ToArray -List $Array -Element $ArrayValues
$Run++
}
return , $Array
}
It essentially converts object into Array of Arrays. Which then makes it trivial to just loop thru rows / columns.
Now it's important that while generally I could probably make Get-AdUser display only values I want in proper order I am working on general use modules (PSWriteWord/PSWriteExcel) and I want people to pass any object to it and not have to care about it too much.
Unless anyone has a better option:
$SpecialData = $Domain.DomainUsersFullList | Select-Object $($Domain.DomainUsersFullList[0]).PSObject.Properties.Name
$($Domain.DomainUsersFullList[0]).PSObject.Properties.Name[12]
$($Domain.DomainUsersFullList[10]).PSObject.Properties.Name[12]
$($SpecialData[0] | Select-Object $($Domain.DomainUsersFullList[0]).PSObject.Properties.Name).PSObject.Properties.Name[12]
$($SpecialData[10] | Select-Object $($Domain.DomainUsersFullList[0]).PSObject.Properties.Name).PSObject.Properties.Name[12]
Basically what this does is copy the order of 1st element and applies same order to each and every new line. This ensures that each object will return properties in same order as 1st element.
Final implementation:
function Format-PSTableConvertType2 {
[CmdletBinding()]
param(
$Object,
[switch] $SkipTitles,
[string[]] $ExcludeProperty,
[switch] $NoAliasOrScriptProperties,
[switch] $DisplayPropertySet,
$OverwriteHeaders
)
#[int] $Run = 0
$Array = New-ArrayList
$Titles = New-ArrayList
if ($NoAliasOrScriptProperties) {$PropertyType = 'AliasProperty', 'ScriptProperty' } else {$PropertyType = ''}
Write-Verbose "Format-PSTableConvertType2 - Option 2 - NoAliasOrScriptProperties: $NoAliasOrScriptProperties"
# Get Titles first (to make sure order is correct for all rows)
if ($OverwriteHeaders) {
$Titles = $OverwriteHeaders
} else {
foreach ($O in $Object) {
if ($DisplayPropertySet -and $O.psStandardmembers.DefaultDisplayPropertySet.ReferencedPropertyNames) {
$ObjectProperties = $O.psStandardmembers.DefaultDisplayPropertySet.ReferencedPropertyNames.Where( { $ExcludeProperty -notcontains $_ } ) #.Name
} else {
$ObjectProperties = $O.PSObject.Properties.Where( { $PropertyType -notcontains $_.MemberType -and $ExcludeProperty -notcontains $_.Name } ).Name
}
foreach ($Name in $ObjectProperties) {
Add-ToArray -List $Titles -Element $Name
}
break
}
# Add Titles to Array (if not -SkipTitles)
if (-not $SkipTitle) {
Add-ToArray -List $Array -Element $Titles
}
}
# Extract data (based on Title)
foreach ($O in $Object) {
$ArrayValues = New-ArrayList
foreach ($Name in $Titles) {
Add-ToArray -List $ArrayValues -Element $O.$Name
}
Add-ToArray -List $Array -Element $ArrayValues
}
return , $Array
}

Switch handling with global arrays

Say I have 2 global arrays:
$Global:Values = #(1..100)
$Global:Test = #(75, 50, 25, 101)
Then I create a Switch by piping $Global:Test into a ForEach loop by using different conditions which I tried troubleshooting to get any kind of response:
$Global:Test | ForEach-Object ($_) {
$_
Switch ($_) {
($_ -contains $Global:Values) {Write-Host "Excellent"}
($_ -in $Global:Values) {Write-Host "Satisfactory"}
($_ -eq "25") {Write-Host "Unsatisfactory"}
Default {Write-Host "Invalid Grade"}
}
}
Output:
75
Invalid Grade
50
Invalid Grade
25
Invalid Grade
101
Invalid Grade
All of these switch statements do not work except the default. I don't know what I'm missing, but I'm sure it's a simple mistake I'm overlooking. Can anyone help me spot this mistake?
The foreach() statement is not the same as the ForEach-Object cmdlet, and you seem to be combining the syntax of both.
Additionally, you're using the switch statement incorrectly.
The part you're trying to match against needs to be a value:
switch ($something)
{
5 { "5 thing" }
(10 + 10) { "20 thing" }
}
or a scriptblock expression that returns a truthy or falsey value:
switch ($something)
{
{ $_ -gt 5 } { "do > 5 thing" }
{ Test-SomeCondition -Value $_ } { "do other thing" }
}
So you want the scriptblock form, but you're using parentheses.
So here's the ForEach-Object form:
$Global:Test | ForEach-Object {
Switch ($_) {
{$_ -contains $Global:Values} {Write-Host "Excellent"}
{$_ -in $Global:Values} {Write-Host "Satisfactory"}
"25" {Write-Host "Unsatisfactory"}
Default {Write-Host "Invalid Grade"}
}
}
or the foreach form:
foreach($value in $Global:Test) {
Switch ($value) {
{$_ -contains $Global:Values} {Write-Host "Excellent"}
{$_ -in $Global:Values} {Write-Host "Satisfactory"}
"25" {Write-Host "Unsatisfactory"}
Default {Write-Host "Invalid Grade"}
}
}
Also of note, the switch statement can actually work directly with arrays, so you don't have to iterate first. Basic example:
switch (1..10)
{
{ $_ -gt 5 } { "Second Half"}
default: { "First Half" }
}
So with yours, maybe:
Switch ($Global:Test) {
{$_ -contains $Global:Values} {Write-Host "Excellent"}
{$_ -in $Global:Values} {Write-Host "Satisfactory"}
"25" {Write-Host "Unsatisfactory"}
Default {Write-Host "Invalid Grade"}
}
Small addendum: stop using Global variables!