Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Powershell - Retain the text of all Enum properties with ConvertTo-Json

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"
}
like image 296
puneet Avatar asked Jun 12 '17 08:06

puneet


1 Answers

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.

like image 158
mklement0 Avatar answered Sep 28 '22 11:09

mklement0