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

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())"
    }
  }
}

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.


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...