Formatting JSON with proper indentation using PowerShell

PowerShell is a powerful scripting language that makes working with data a breeze, allowing you to import and export to a variety of common file formats such as CSV, XML, and JSON.

However, when it comes to JSON, the formatting applied by the built-in ConvertTo-Json cmdlet leaves a lot to be desired. While this may not matter in many cases, it does make a difference in others. For example, consider a scenario where you want to automatically generate a JSON configuration file that users can subsequently edit. In this case, proper formatting and indentation are highly desirable.

In this post, I will demonstrate what a poorly formatted JSON string produced by ConvertTo-Json looks like and how you can resolve the issue by sanitising the output with a custom JSON formatting function.

Note, if you are using PowerShell 7+ the issue highlighted in this post is not of concern; only Windows PowerShell is affected.

Bad JSON formatting

Okay, so when I say “bad JSON formatting”, what do I mean exactly?

Consider the following PowerShell code which first creates a hash table named $settings, then converts the hash table to JSON, and finally outputs the results to a JSON file.

$settings = [Ordered]@{
    EnableSSL = $true
    MaxThreads = 8
    ConnectionStrings = @{
        DefaultConnection = "Server=SERVER_NAME;Database=DATABASE_NAME;Trusted_Connection=True;"
    }
}
 
$configFilePath = Join-Path -Path $PSScriptRoot -ChildPath "config.json"
 
$settings | ConvertTo-Json | Out-File $configFilePath

Note the use of the [Ordered] attribute which allows us to guarantee that the properties will be serialised to JSON in the same order in which they are specified within the hash table.

When the above code is executed a ‘config.json’ file will be created in the script directory with the following contents.

{
    "EnableSSL":  true,
    "MaxThreads":  8,
    "ConnectionStrings":  {
                              "DefaultConnection":  "Server=SERVER_NAME;Database=DATABASE_NAME;Trusted_Connection=True;"
                          }
}

Well, that doesn’t look great. The indentation is all over the shop here. I don’t know who thought it was a good idea to format the JSON in this manner and, in addition, failed to provide a way to customise the formatting.

The only way to get a different result with Windows PowerShell is by specifying the -Compress parameter when invoking the ConvertTo-Json cmdlet, which results in the following output.

{"EnableSSL":true,"MaxThreads":8,"ConnectionStrings":{"DefaultConnection":"Server=SERVER_NAME;Database=DATABASE_NAME;Trusted_Connection=True;"}}

Of course, this output isn’t suitable for the configuration file scenario mentioned earlier. For a configuration file with many settings, it would be very difficult for a user to understand this structure and find the correct settings to edit.

So, how can we fix this?

Let’s take a look at the solution in the next section.

Formatting the JSON

To fix the issue for Windows PowerShell, we can implement a custom function, as shown below.

function Format-Json
{
    <#
    .SYNOPSIS
        Applies proper formatting to a JSON string with the specified indentation.
 
    .DESCRIPTION
        The `Format-Json` function takes a JSON string as input and formats it with the specified level of indentation. 
        The function processes each line of the JSON string, adjusting the indentation level based on the structure of the JSON.
 
    .PARAMETER Json
        The JSON string to be formatted.
        This parameter is mandatory and accepts input from the pipeline.
 
    .PARAMETER Indentation
        Specifies the number of spaces to use for each indentation level.
        The value must be between 1 and 1024. 
        The default value is 2.
 
    .EXAMPLE
        $formattedJson = Get-Content -Path 'config.json' | Format-Json -Indentation 4
        This example reads the JSON content from a file named 'config.json', formats it with an 
        indentation level of 4 spaces, and stores the result in the `$formattedJson` variable.
 
    .EXAMPLE
        @'
        {
            "EnableSSL":  true,
            "MaxThreads":  8,
            "ConnectionStrings":  {
                                      "DefaultConnection":  "Server=SERVER_NAME;Database=DATABASE_NAME;Trusted_Connection=True;"
                                  }
        }
        '@ | Format-Json
        This example formats an inline JSON string with the default indentation level of 2 spaces.
 
    .NOTES
        This function assumes that the input string is valid JSON.
    #>
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$Json,
 
        [ValidateRange(1, 1024)]
        [Int]$Indentation = 2
    )
 
    $lines = $Json -split '\n'
 
    $indentLevel = 0
 
    $result = $lines | ForEach-Object `
    {
        if ($_ -match "[\}\]]")
        {
            $indentLevel--
        }
 
        $line = (' ' * $indentLevel * $Indentation) + $_.TrimStart().Replace(":  ", ": ")
 
        if ($_ -match "[\{\[]")
        {
            $indentLevel++
        }
 
        return $line
    }
 
    return $result -join "`n"
}

The Format-Json function defined above is simple yet effective.

The function takes a JSON string as its input via the $Json parameter and can accept this value from the pipeline. An optional $Indentation parameter allows the level of indentation to be specified and has a default value of 2 spaces.

The body of the function splits the JSON lines on the new-line character and then iterates over the lines.

For each line, if a closing brace or bracket is found, the indentation level is decremented. The correct number of spaces for the current indentation level is then calculated and any leading whitespace is trimmed from the original line. If an opening brace or bracket is found, the indentation level is incremented.

Lastly, the processed lines are joined together again with the new-line character and returned.

Applying the formatting

Now that we have the Format-Json defined, we can pipe the results of the ConvertTo-Json cmdlet into it, in order to apply proper formatting to the JSON string that we output to the file, as shown in the updated code below.

$settings = [Ordered]@{
    EnableSSL = $true
    MaxThreads = 8
    ConnectionStrings = @{
        DefaultConnection = "Server=SERVER_NAME;Database=DATABASE_NAME;Trusted_Connection=True;"
    }
}
 
$configFilePath = Join-Path -Path $PSScriptRoot -ChildPath "config.json"
 
$settings | ConvertTo-Json | Format-Json | Out-File $configFilePath

The contents of the file will now look like the following.

{
  "EnableSSL": true,
  "MaxThreads": 8,
  "ConnectionStrings": {
    "DefaultConnection": "Server=SERVER_NAME;Database=DATABASE_NAME;Trusted_Connection=True;"
  }
}

Ah, much better!

With the proper formatting applied, we can now easily understand the structure of the JSON file and how the settings are grouped when there is nesting. The contents of the file look much cleaner without the superfluous indentation.

Summary

In this post, I have demonstrated the inherent issues with the ConvertTo-Json cmdlet when it comes to the formatting of the JSON strings that it produces.

To work around the formatting issues, I defined a simple Format-Json function which can accept a JSON string as its input and applies proper formatting to it, according to the specified level of indentation.

Before wrapping up, it is important to note that the custom Format-Json function should correctly handle a JSON string that is output via the ConvertTo-Json cmdlet without compression; it is not designed to handle compressed JSON strings in its current form. Aside from this, I am sure there are further improvements that could be made to support other scenarios, so please feel free to let me know if there is anything else you would add to the function.

In closing, don’t forget that if you are in a position to use PowerShell 7+ instead of Windows PowerShell, then you won’t need to worry about the issue covered in this post. The latest cross-platform PowerShell version applies the expected JSON formatting as standard!


I hope you enjoyed this post! Comments are always welcome and I respond to all questions.

If you like my content and it helped you out, please check out the button below 🙂

Comments