Powershell - Retain the text of all Enum properties with ConvertTo-Json - 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"
}

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 7.2: ConvertFrom-Json - Date Handling

With Powershell 7.2 there seems to be a change in how a JSON is deserialized into an object in terms of dates -> instead of string it is now datetime. But I want to have the "old" behavior, i.e. that it is handled as string and NOT datetime.
How can I achieve that when using ConvertFrom-Json in Powershell 7.2 all dates are deserialized as string and not datetime?
EDIT:
$val = '{ "date":"2022-09-30T07:04:23.571+00:00" }' | ConvertFrom-Json
$val.date.GetType().FullName
This is actually a known issue, see: #13598 Add a -DateKind parameter to ConvertFrom-Json to control how System.DateTime / System.DateTimeOffset values are constructed. Yet I think there is no easy solution for this. One thing you might do is just invoke (Windows) PowerShell. Which isn't currently straights forward as well therefore I have created a small wrapper to send and receive complex objects between PowerShell sessions (see also my #18460 Invoke-PowerShell purpose):
function Invoke-PowerShell ($Command) {
$SerializeOutput = #"
`$Output = $Command
[System.Management.Automation.PSSerializer]::Serialize(`$Output)
"#
$Bytes = [System.Text.Encoding]::Unicode.GetBytes($SerializeOutput)
$EncodedCommand = [Convert]::ToBase64String($Bytes)
$PSSerial = PowerShell -EncodedCommand $EncodedCommand
[System.Management.Automation.PSSerializer]::Deserialize($PSSerial)
}
Usage:
Invoke-PowerShell { '{ "date":"2022-09-30T07:04:23.571+00:00" }' | ConvertFrom-Json }
date
----
2022-09-30T07:04:23.571+00:00
Update
As commented by mklement0, I clearly complicated the answer.
Calling via powershell.exe is a pragmatic workaround (albeit slow and Windows-only), but note that you don't need a helper function: if you pass a script block to powershell.exe (or pwsh.exe) from PowerShell, Based64 CLIXML-based serialization happens automatically behind the scenes: try powershell.exe -noprofile { $args | ConvertFrom-Json } -args '{ "date":"2022-09-30T07:04:23.571+00:00" }' For that reason, I don't think there's a need for an Invoke-PowerShell cmdlet.
$Json = '{ "date":"2022-09-30T07:04:23.571+00:00" }'
powershell.exe -noprofile { $args | ConvertFrom-Json } -args $Json
date
----
2022-09-30T07:04:23.571+00:00
iRon's helpful answer provides a pragmatic solution via the Windows PowerShell CLI, powershell.exe, relying on the fact that ConvertFrom-Json there does not automatically transform ISO 8601-like timestamp strings to [datetime] instances.
Hopefully, the proposal in the GitHub issue he links to, #13598, will be implemented in the future, which would then simplify the solution to:
# NOT YET IMPLEMENTED as of PowerShell 7.2.x
'{ "date":"2022-09-30T07:04:23.571+00:00" }' |
ConvertFrom-Json -DateTimeKind None
However, a powershell.exe workaround has two disadvantages: (a) it is slow (a separate PowerShell instance in a child process must be launched), and (b) it is Windows-only. The solution below is a generalization of your own approach that avoids these problems.
Here's a generalization of your own in-process approach:
It injects a NUL character ("`0") at the start of each string that matches the pattern of a timestamp - the assumption is that the input itself never contains such characters, which is fair to assume.
This, as in your approach, prevents ConvertFrom-Json from recognizing timestamp strings as such, and leaves them untouched.
The [pscustomobject] graph that ConvertFrom-Json outputs must then be post-processed in order to remove the injected NUL characters again.
This is achieved with a ForEach-Object call that contains a helper script block that recursively walks the object graph, which has the advantage that it works with JSON input whose timestamp strings may be at any level of the hierarchy (i.e. they may also be in properties of nested objects).
Note: The assumption is that the timestamp strings are only ever contained as property values in the input; more work would be needed if you wanted to handle input JSON such as '[ "2022-09-30T07:04:23.571+00:00" ]' too, where the strings are input objects themselves.
# Sample JSON.
$val = '{ "date":"2022-09-30T07:04:23.571+00:00" }'
$val -replace '"(?=\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}")', "`"`0" | #"
ConvertFrom-Json |
ForEach-Object {
# Helper script block that walks the object graph
$sb = {
foreach ($o in $args[0]) {
if ($o -is [Array]) { # nested array -> recurse
foreach ($el in $o) { & $sb $el } # recurse
}
elseif ($o -is [System.Management.Automation.PSCustomObject]) {
foreach ($prop in $o.psobject.Properties) {
if ($prop.Value -is [Array]) {
foreach ($o in $prop.Value) { & $sb $o } # nested array -> recurse
}
elseif ($prop.Value -is [System.Management.Automation.PSCustomObject]) {
& $sb $prop.Value # nested custom object -> recurse
}
elseif ($prop.Value -is [string] -and $prop.Value -match '^\0') {
$prop.Value = $prop.Value.Substring(1) # Remove the NUL again.
}
}
}
}
}
# Call the helper script block with the input object.
& $sb $_
# Output the modified object.
if ($_ -is [array]) {
# Input object was array as a whole (implies use of -NoEnumerate), output as such.
, $_
} else {
$_
}
}
Based on the input from #zett42 here my solution:
Assuming we know the regex pattern of the date used in the JSON I get the JSON as string, add a prefix so that ConvertFrom-Json does not convert dates to datetime but keeps it as string, convert it with ConvertFrom-Json to a PSCustomObject, do whatever I need to do on the object, serialize it back to a JSON string with ConvertTo-Json and then remove the prefix again.
[string]$json = '{ "date":"2022-09-30T07:04:23.571+00:00", "key1": "value1" }'
[string]$jsonWithDatePrefix = $json -replace '"(\d+-\d+.\d+T\d+:\d+:\d+\.\d+\+\d+:\d+)"', '"#$1"'
[pscustomobject]$jsonWithDatePrefixAsObject = $jsonWithDatePrefix | ConvertFrom-Json
$jsonWithDatePrefixAsObject.key1 = "value2"
[string]$updatedJsonString = $jsonWithDatePrefixAsObject | ConvertTo-Json
[string]$updatedJsonStringWithoutPrefix = $updatedJsonString -replace '"(#)(\d+-\d+.\d+T\d+:\d+:\d+\.\d+\+\d+:\d+)"', '"$2"'
Write-Host $updatedJsonStringWithoutPrefix
Two additional ways to change the date format:
Get-Node
Using this Get-Node which is quiet similar to mklement0 recursive function:
$Data = ConvertFrom-Json $Json
$Data |Get-Node -Where { $_.Value -is [DateTime] } | ForEach-Object {
$_.Value = GetDate($_.Value) -Format 'yyyy-MM-ddTHH\:mm\:ss.fffzzz' -AsUTC
}
$Data
DIY
Or do-it-yourself and build your own Json deserializer:
function ConvertFrom-Json {
[CmdletBinding()][OutputType([Object[]])] param(
[Parameter(ValueFromPipeLine = $True, Mandatory = $True)][String]$InputObject,
[String]$DateFormat = 'yyyy-MM-ddTHH\:mm\:ss.fffffffzzz', # Default: ISO 8601, https://www.newtonsoft.com/json/help/html/datesinjson.htm
[Switch]$AsLocalTime,
[Switch]$AsOrdered
)
function GetObject($JObject) {
switch ($JObject.GetType().Name) {
'JValue' {
switch ($JObject.Type) {
'Boolean' { $JObject.Value }
'Integer' { 0 + $JObject.Value } # https://github.com/PowerShell/PowerShell/issues/14264
'Date' { Get-Date $JObject.Value -Format $DateFormat -AsUTC:(!$AsLocalTime) } # https://github.com/PowerShell/PowerShell/issues/13598
Default { "$($JObject.Value)" }
}
}
'JArray' {
,#( $JObject.ForEach{ GetObject $_ } )
}
'JObject' {
$Properties = [Ordered]#{}
$JObject.ForEach{ $Properties[$_.Name] = GetObject $_.Value }
if ($AsOrdered) { $Properties } else { [PSCustomObject]$Properties } # https://github.com/PowerShell/PowerShell/pull/17405
}
}
}
GetObject ([Newtonsoft.Json.Linq.JObject]::Parse($InputObject))
}
Usage:
ConvertFrom-Json $Json -DateFormat 'yyyy-MM-ddTHH\:mm\:ss.fffzzz' |ConvertTo-Json -Depth 9

Easy way to reference JSON arrays [duplicate]

I want to get a JSON representation of a Hashtable such as this:
#{Path="C:\temp"; Filter="*.js"}
ConvertTo-Json results in:
{
"Path": "C:\\temp",
"Filter": "*.js"
}
However, if you convert that JSON string back with ConvertFrom-Json you don't get a HashTable but a PSCustomObject.
So how can one reliably serialize the above Hashmap?
$json = #{Path="C:\temp"; Filter="*.js"} | ConvertTo-Json
$hashtable = #{}
(ConvertFrom-Json $json).psobject.properties | Foreach { $hashtable[$_.Name] = $_.Value }
Adapted from PSCustomObject to Hashtable
A little late to the discussion here, but in PowerShell 6 (Core) there is a -AsHashtable parameter in ConvertFrom-Json.
JavaScriptSerializer is available since .NET3.5 (may be installed on XP, included in Win7 and newer), it's several times faster than Convert-FromJSON and it properly parses nested objects, arrays etc.
function Parse-JsonFile([string]$file) {
$text = [IO.File]::ReadAllText($file)
$parser = New-Object Web.Script.Serialization.JavaScriptSerializer
$parser.MaxJsonLength = $text.length
Write-Output -NoEnumerate $parser.DeserializeObject($text)
}
The answer for this post is a great start, but is a bit naive when you start getting more complex json representations.
The code below will parse nested json arrays and json objects.
[CmdletBinding]
function Get-FromJson
{
param(
[Parameter(Mandatory=$true, Position=1)]
[string]$Path
)
function Get-Value {
param( $value )
$result = $null
if ( $value -is [System.Management.Automation.PSCustomObject] )
{
Write-Verbose "Get-Value: value is PSCustomObject"
$result = #{}
$value.psobject.properties | ForEach-Object {
$result[$_.Name] = Get-Value -value $_.Value
}
}
elseif ($value -is [System.Object[]])
{
$list = New-Object System.Collections.ArrayList
Write-Verbose "Get-Value: value is Array"
$value | ForEach-Object {
$list.Add((Get-Value -value $_)) | Out-Null
}
$result = $list
}
else
{
Write-Verbose "Get-Value: value is type: $($value.GetType())"
$result = $value
}
return $result
}
if (Test-Path $Path)
{
$json = Get-Content $Path -Raw
}
else
{
$json = '{}'
}
$hashtable = Get-Value -value (ConvertFrom-Json $json)
return $hashtable
}
I believe the solution presented in Converting JSON to a hashtable is closer to the PowerShell 6.0 implementation of ConvertFrom-Json
I tried with several JSON sources and I always got the right hashtable.
$mappings = #{
Letters = (
"A",
"B")
Numbers = (
"1",
"2",
"3")
Yes = 1
False = "0"
}
# TO JSON
$jsonMappings = $mappings | ConvertTo-JSON
$jsonMappings
# Back to hashtable
# In PowerShell 6.0 would be:
# | ConvertFrom-Json -AsHashtable
$jsonMappings | ConvertFrom-Json -As hashtable
you can write a function convert psobject to hashtable.
I wrote a answer here:enter link description here

PowerShell JSON adding value format

I am adding data to a json file. I do this by
$blockcvalue =#"
{
"connectionString":"server=(localdb)\\mssqllocaldb; Integrated Security=true;Database=$database;"
}
"#
$ConfigJson = Get-Content C:\Users\user\Desktop\myJsonFile.json -raw | ConvertFrom-Json
$ConfigJson.data | add-member -Name "database" -value (Convertfrom-Json $blockcvalue) -MemberType NoteProperty
$ConfigJson | ConvertTo-Json| Set-Content C:\Users\user\Desktop\myJsonFile.json
But the format comes out like this:
{
"data": {
"database": {
"connectionString": "server=(localdb)\\mssqllocaldb; Integrated Security=true;Database=mydatabase;"
}
}
}
but I need it like this:
{
"data": {
"database":"server=(localdb)\\mssqllocaldb; Integrated Security=true;Database=mydatabase;"
}
}
}
Can someone help please?
Here's my function to prettify JSON output:
function Format-Json {
<#
.SYNOPSIS
Prettifies JSON output.
.DESCRIPTION
Reformats a JSON string so the output looks better than what ConvertTo-Json outputs.
.PARAMETER Json
Required: [string] The JSON text to prettify.
.PARAMETER Indentation
Optional: The number of spaces to use for indentation. Defaults to 2.
.PARAMETER AsArray
Optional: If set, the output will be in the form of a string array, otherwise a single string is output.
.EXAMPLE
$json | ConvertTo-Json | Format-Json -Indentation 4
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[string]$Json,
[int]$Indentation = 2,
[switch]$AsArray
)
# If the input JSON text has been created with ConvertTo-Json -Compress
# then we first need to reconvert it without compression
if ($Json -notmatch '\r?\n') {
$Json = ($Json | ConvertFrom-Json) | ConvertTo-Json -Depth 100
}
$indent = 0
$Indentation = [Math]::Abs($Indentation)
$regexUnlessQuoted = '(?=([^"]*"[^"]*")*[^"]*$)'
$result = $Json -split '\r?\n' |
ForEach-Object {
# If the line contains a ] or } character,
# we need to decrement the indentation level unless it is inside quotes.
if ($_ -match "[}\]]$regexUnlessQuoted") {
$indent = [Math]::Max($indent - $Indentation, 0)
}
# Replace all colon-space combinations by ": " unless it is inside quotes.
$line = (' ' * $indent) + ($_.TrimStart() -replace ":\s+$regexUnlessQuoted", ': ')
# If the line contains a [ or { character,
# we need to increment the indentation level unless it is inside quotes.
if ($_ -match "[\{\[]$regexUnlessQuoted") {
$indent += $Indentation
}
$line
}
if ($AsArray) { return $result }
return $result -Join [Environment]::NewLine
}
Use it like so:
$ConfigJson | ConvertTo-Json | Format-Json | Set-Content C:\Users\user\Desktop\myJsonFile.json
Replace
(Convertfrom-Json $blockcvalue)
with
(Convertfrom-Json $blockcvalue).connectionString
Then your output object's data.database property will directly contain the "server=(localdb)\\..." value, as desired, not via a nested object that has a connectionString property.
There is one simple Newtonsoft.Json Parser which makes it rly simple to get required format:
Import-Module Newtonsoft.Json
$path = "C:\..."
$json = Get-Content -Path $path -Raw
$parsedJson = [Newtonsoft.Json.Linq.JToken]::Parse($json);
Set-Content $path $parsedJson.ToString();
Enjoy ;)

Create Hashtable from JSON

I want to get a JSON representation of a Hashtable such as this:
#{Path="C:\temp"; Filter="*.js"}
ConvertTo-Json results in:
{
"Path": "C:\\temp",
"Filter": "*.js"
}
However, if you convert that JSON string back with ConvertFrom-Json you don't get a HashTable but a PSCustomObject.
So how can one reliably serialize the above Hashmap?
$json = #{Path="C:\temp"; Filter="*.js"} | ConvertTo-Json
$hashtable = #{}
(ConvertFrom-Json $json).psobject.properties | Foreach { $hashtable[$_.Name] = $_.Value }
Adapted from PSCustomObject to Hashtable
A little late to the discussion here, but in PowerShell 6 (Core) there is a -AsHashtable parameter in ConvertFrom-Json.
JavaScriptSerializer is available since .NET3.5 (may be installed on XP, included in Win7 and newer), it's several times faster than Convert-FromJSON and it properly parses nested objects, arrays etc.
function Parse-JsonFile([string]$file) {
$text = [IO.File]::ReadAllText($file)
$parser = New-Object Web.Script.Serialization.JavaScriptSerializer
$parser.MaxJsonLength = $text.length
Write-Output -NoEnumerate $parser.DeserializeObject($text)
}
The answer for this post is a great start, but is a bit naive when you start getting more complex json representations.
The code below will parse nested json arrays and json objects.
[CmdletBinding]
function Get-FromJson
{
param(
[Parameter(Mandatory=$true, Position=1)]
[string]$Path
)
function Get-Value {
param( $value )
$result = $null
if ( $value -is [System.Management.Automation.PSCustomObject] )
{
Write-Verbose "Get-Value: value is PSCustomObject"
$result = #{}
$value.psobject.properties | ForEach-Object {
$result[$_.Name] = Get-Value -value $_.Value
}
}
elseif ($value -is [System.Object[]])
{
$list = New-Object System.Collections.ArrayList
Write-Verbose "Get-Value: value is Array"
$value | ForEach-Object {
$list.Add((Get-Value -value $_)) | Out-Null
}
$result = $list
}
else
{
Write-Verbose "Get-Value: value is type: $($value.GetType())"
$result = $value
}
return $result
}
if (Test-Path $Path)
{
$json = Get-Content $Path -Raw
}
else
{
$json = '{}'
}
$hashtable = Get-Value -value (ConvertFrom-Json $json)
return $hashtable
}
I believe the solution presented in Converting JSON to a hashtable is closer to the PowerShell 6.0 implementation of ConvertFrom-Json
I tried with several JSON sources and I always got the right hashtable.
$mappings = #{
Letters = (
"A",
"B")
Numbers = (
"1",
"2",
"3")
Yes = 1
False = "0"
}
# TO JSON
$jsonMappings = $mappings | ConvertTo-JSON
$jsonMappings
# Back to hashtable
# In PowerShell 6.0 would be:
# | ConvertFrom-Json -AsHashtable
$jsonMappings | ConvertFrom-Json -As hashtable
you can write a function convert psobject to hashtable.
I wrote a answer here:enter link description here

Split Period-Delimited Nodes To JSON Object

I have many string entries (this are namespace/class trees) that look like the following:
appsystem
appsystem.applications
appsystem.applications.APPactivities
appsystem.applications.APPmanager
appsystem.applications.APPmodels
appsystem.applications.MAPmanager
appsystem.applications.MAPmanager.maphub
appsystem.applications.MAPmanager.mapmanager
appsystem.applications.pagealertsmanager
appsystem.authentication
appsystem.authentication.manager
appsystem.authentication.manager.encryptionmanager
appsystem.authentication.manager.sso
appsystem.authentication.manager.tokenmanager
But, I need the final output to be like:
{
"name": "appsystem",
"children": [
{
"name": "applications",
"children": [
{"name": "APPactivities"},
{"name": "APPmanager"},
{"name": "APPmodels"},
{"name": "MAPmanager",
"children": [
{"name": "maphub"},
{"name": "mapmanager"}
]},
{"name": "pagealertsmanager"}
]
},
{
"name": "authentication",
"children": [
{"name": "manager",
"children": [
{"name": "encryptionmanager"},
{"name": "sso"},
{"name": "tokenmanager"}
]}
]
}
]
}
The total nodes can be any number.
I am assuming I am going to need recursion but I am at a loss on where even to begin.
This builds up nested lists, PowerShell ConvertTo-JSON flattens the outer list.
You can change the $Line in $s to $line in (Get-Content input.txt).
But I think this does it:
$s = #'
appsystem
appsystem.applications
appsystem.applications.APPactivities
appsystem.applications.APPmanager
appsystem.applications.APPmodels
appsystem.applications.MAPmanager
appsystem.applications.MAPmanager.maphub
appsystem.applications.MAPmanager.mapmanager
appsystem.applications.pagealertsmanager
appsystem.authentication
appsystem.authentication.manager
appsystem.authentication.manager.encryptionmanager
appsystem.authentication.manager.sso
appsystem.authentication.manager.tokenmanager
'# -split "`r`n"
$TreeRoot = New-Object System.Collections.ArrayList
foreach ($Line in $s) {
$CurrentDepth = $TreeRoot
$RemainingChunks = $Line.Split('.')
while ($RemainingChunks)
{
# If there is a dictionary at this depth then use it, otherwise create one.
$Item = $CurrentDepth | Where-Object {$_.name -eq $RemainingChunks[0]}
if (-not $Item)
{
$Item = #{name=$RemainingChunks[0]}
$null = $CurrentDepth.Add($Item)
}
# If there will be child nodes, look for a 'children' node, or create one.
if ($RemainingChunks.Count -gt 1)
{
if (-not $Item.ContainsKey('children'))
{
$Item['children'] = New-Object System.Collections.ArrayList
}
$CurrentDepth = $Item['children']
}
$RemainingChunks = $RemainingChunks[1..$RemainingChunks.Count]
}
}
$TreeRoot | ConvertTo-Json -Depth 1000
Edit: It's too slow? I tried some random pausing profiling and found (not too surprisingly) that it's the inner nested loop, which searches children arrays for matching child nodes, which is being hit too many times.
This is a redesigned version which still builds the tree, and this time it also builds a TreeMap hashtable of shortcuts into the tree, to all the previously build nodes, so it can jump right too them instead of searching the children lists for them.
I made a testing file, some 20k random lines. Original code processed it in 108 seconds, this one does it in 1.5 seconds and the output matches.
$TreeRoot = New-Object System.Collections.ArrayList
$TreeMap = #{}
foreach ($line in (Get-Content d:\out.txt)) {
$_ = ".$line" # easier if the lines start with a dot
if ($TreeMap.ContainsKey($_)) # Skip duplicate lines
{
continue
}
# build a subtree from the right. a.b.c.d.e -> e then d->e then c->d->e
# keep going until base 'a.b' reduces to something already in the tree, connect new bit to that.
$LineSubTree = $null
$TreeConnectionPoint = $null
do {
$lastDotPos = $_.LastIndexOf('.')
$leaf = $_.Substring($lastDotPos + 1)
$_ = $_.Substring(0, $lastDotPos)
# push the leaf on top of the growing subtree
$LineSubTree = if ($LineSubTree) {
#{"name"=$leaf; "children"=([System.Collections.ArrayList]#($LineSubTree))}
} else {
#{"name"=$leaf}
}
$TreeMap["$_.$leaf"] = $LineSubTree
} while (!($TreeConnectionPoint = $TreeMap[$_]) -and $_)
# Now we have a branch built to connect in to the existing tree
# but is there somewhere to put it?
if ($TreeConnectionPoint)
{
if ($TreeConnectionPoint.ContainsKey('children'))
{
$null = $TreeConnectionPoint['children'].Add($LineSubTree)
} else {
$TreeConnectionPoint['children'] = [System.Collections.ArrayList]#($LineSubTree)
}
} else
{ # nowhere to put it, this is a new root level connection
$null = $TreeRoot.Add($LineSubTree)
}
}
$TreeRoot | ConvertTo-Json -Depth 100
(#mklement0's code takes 103 seconds and produces a wildly different output - 5.4M characters of JSON instead of 10.1M characters of JSON. [Edit: because my code allows multiple root nodes in a list which my test file has, and their code does not allow that])
Auto-generated PS help links from my codeblock (if available):
New-Object (in module Microsoft.PowerShell.Utility)
Get-Content (in module Microsoft.PowerShell.Management)
ConvertTo-Json (in module Microsoft.PowerShell.Utility)
To complement TessellatingHeckler's great answer with an alternative implementation that uses a recursive function.
The emphasis is on modularity and terseness, not performance.[1]
# Outer function that loops over all paths and builds up a one or more nested
# hashtables reflecting the path hierarchy, which are converted to JSON on output.
# Note that only a single JSON object is output if all paths share the same root
# component; otherwise, a JSON *array* is output.
function convert-PathsToNestedJsonObject([string[]] $paths) {
$hts = New-Object Collections.ArrayList
$paths.ForEach({
$rootName = $_.split('.')[0]
$ht = $hts.Where({ $_.name -eq $rootName }, 'First')[0]
if (-not $ht) { [void] $hts.Add(($ht = #{})) }
convert-PathToNestedHashtable $ht $_
})
$hts | ConvertTo-Json -Depth 100
}
# Recursive helper function that takes a path such as "appsystem.applications"
# and converts it into a nested hashtable with keys "name" and "children" to
# reflect the path hierarchy.
function convert-PathToNestedHashtable([hashtable] $ht, [string] $path) {
$name, $childName, $rest = $path -split '\.', 3
$ht.name = $name
if ($childName) {
if ($ht.children) {
$htChild = $ht.children.Where({ $_.name -eq $childName }, 'First')[0]
} else {
$ht.children = New-Object Collections.ArrayList
$htChild = $null
}
if (-not $htChild) {
[void] $ht.children.Add(($htChild = #{}))
}
convert-PathToNestedHashtable $htChild "$childName.$rest"
}
}
# Call the outer function with the input paths (assumed to be stored in $paths).
convert-PathsToNestedJsonObject $paths
[1] One deliberate type of optimization is applied, which, however, still keeps the code terse:
PSv4+ offers the (little-known) array methods .ForEach() and .Where(), which are not only noticeably faster than their cmdlet counterparts ForEach-Object and Where-Object, but also offer additional features.
Specifically:
$paths.ForEach({ ... }) is used instead of
$paths | ForEach-Object { ... }
$ht.children.Where({ $_.name -eq $childName }, 'First')[0] is used instead of
$ht.children | Where-Object { $_.name -eq $childName } | Select-Object -First 1