17

I have a following file in JSON format:

Before ConvertTo-JSON:

[
    {
        "Yura": {
            "Cashier": {
                "branch": "release/Retail-v4.0",
                "configuration": "RetailDemo Debug",
                "datetime_deployed": "Apr 18 2018 07:45:05",
                "deployed_by": "anonymous",
                "host": "cashier2-retail4.testing.aws.com",
                "job": "http://jenkins-testing.aws.com:8080/job/CashierDeployment",
                "lineserver": "",
                "messagebus": "",
                "product": "Cashier",
                "publish_profile": "cashier2.retail.dev.pubxml"
            },
            "ContentManager": {
                "branch": "release/Retail-v3.31.1",
                "configuration": "RetailDemo Debug",
                "datetime_deployed": "Jan 17 2018 11:59:24",
                "deployed_by": "anonymous",
                "host": "contentmanager2-retail3.testing.aws.com",
                "job": "http://jenkins-testing.aws.com:8080/job/ContentManagerDeployment",
                "lineserver": "",
                "messagebus": "",
                "product": "ContentManager",
                "publish_profile": "..\\ContentManager.PublishProfiles\\contentmanager2.retail5.dev.pubxml"
            }
        }
    }
]

after using this code for manipulation with data:

$json = Get-Content 'D:\script\test.json'  -encoding utf8 | ConvertFrom-Json
$json.yura.ContentManager.branch = 'test'

I save JSON into another file:

$json | convertto-json | set-content "D:\script\test1.json" -encoding utf8

Problem is, that after saving file, format become broken:

{
    "Yura":  {
                 "Cashier":  {
                                 "branch":  "release/Retail-v4.0",
                                 "configuration":  "RetailDemo Debug",
                                 "datetime_deployed":  "Apr 18 2018 07:45:05",
                                 "deployed_by":  "anonymous",
                                 "host":  "cashier2-retail4.testing.aws.com",
                                 "job":  "http://jenkins-testing.aws.com:8080/job/CashierDeployment",
                                 "lineserver":  "",
                                 "messagebus":  "",
                                 "product":  "Cashier",
                                 "publish_profile":  "cashier2.retail.dev.pubxml"
                             },
                 "ContentManager":  {
                                        "branch":  "test",
                                        "configuration":  "RetailDemo Debug",
                                        "datetime_deployed":  "Jan 17 2018 11:59:24",
                                        "deployed_by":  "anonymous",
                                        "host":  "contentmanager2-retail3.testing.aws.com",
                                        "job":  "http://jenkins-testing.aws.com:8080/job/ContentManagerDeployment",
                                        "lineserver":  "",
                                        "messagebus":  "",
                                        "product":  "ContentManager",
                                        "publish_profile":  "..\\ContentManager.PublishProfiles\\contentmanager2.retail5.dev.pubxml"
                                    }
             }
}

My question is - how to preserve source formatting in PowerShell?

Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
Vasyl Stepulo
  • 1,493
  • 1
  • 23
  • 43

6 Answers6

33

Since your original json contains an array with only one element, PowerShell will condense that to become just this one element. If in your output you want it to be an array again, use rokumaru's good answer.

However, PowerShell's ConvertTo-Json does not produce pretty formatted json and for that I have written a helper function some time ago:

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 Minify
        Optional: Returns the json string compressed.
    .PARAMETER Indentation
        Optional: The number of spaces (1..1024) to use for indentation. Defaults to 4.
    .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 2
    #>
    [CmdletBinding(DefaultParameterSetName = 'Prettify')]
    Param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string]$Json,

        [Parameter(ParameterSetName = 'Minify')]
        [switch]$Minify,

        [Parameter(ParameterSetName = 'Prettify')]
        [ValidateRange(1, 1024)]
        [int]$Indentation = 4,

        [Parameter(ParameterSetName = 'Prettify')]
        [switch]$AsArray
    )

    if ($PSCmdlet.ParameterSetName -eq 'Minify') {
        return ($Json | ConvertFrom-Json) | ConvertTo-Json -Depth 100 -Compress
    }

    # 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
    $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 this:

$json = Get-Content 'D:\script\test.json' -Encoding UTF8 | ConvertFrom-Json
$json.yura.ContentManager.branch = 'test'

# recreate the object as array, and use the -Depth parameter (your json needs 3 minimum)
ConvertTo-Json @($json) -Depth 3 | Format-Json | Set-Content "D:\script\test1.json" -Encoding UTF8

# instead of using '@($json)' you can of course also recreate the array by adding the square brackets manually:
# '[{0}{1}{0}]' -f [Environment]::NewLine, ($json | ConvertTo-Json -Depth 3) | 
#        Format-Json | Set-Content "D:\script\test1.json" -Encoding UTF8
Theo
  • 57,719
  • 8
  • 24
  • 41
  • this code is adding the json in a array brackets [ ], is there a way to not to enclose the overall json in to the array brackets? – wehelpdox Jun 18 '22 at 02:07
  • 1
    @wehelpdox That was the whole point of the question.. If you don't want the brackets (so if it is **not** to become an array), simply leave out the `@()` array operator like `ConvertTo-Json $json -Depth 3 | Format-Json | Set-Content "D:\script\test1.json" -Encoding UTF8` – Theo Jun 18 '22 at 12:30
5

If the whole is an array and the element is a single json file, it is awkward.
If you use a pipeline, it is not treated as an array.

$json | ConvertTo-Json -Depth 10 # bad

and since it is not a normal array, so just passing it as a parameter won't work.

ConvertTo-Json $json -Depth 10  # bad

It works well if you re-create the array.

ConvertTo-Json @($json) -Depth 10  # good
rokumaru
  • 1,244
  • 1
  • 8
  • 11
5

I found Theo's excellent answer results in faulty indentation for empty [] or {} or in-line arrays.

Example input:

$JsonA = '{
    "Henk": "test",
    "Piet": [],
    "blub": {},
    "Joop": [
        {
            "a": "aaa",
            "b": [],
            "z": "lsjkal"
        }
    ],
    "Klaas": [
        [[],["a"],["b","c"]],
        ["a","b"],
        ["c"],
        [],
        "henk"
    ],
}'

$JsonA | Format-Json

Would result in:

{
    "Henk": "test",
"Piet": [],
"blub": {},
    "Joop": [
        {
            "a": "aaa",
        "b": [],
            "z": "lsjkal"
        }
    ],
    "Klaas": [
    [[],["a"],["b","c"]],
    ["a","b"],
    ["c"],
    [],
        "henk"
    ],
}

Wrote a fix to make sure indentation goes right when line contains both [/{ AND ]/} :

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 Minify
        Optional: Returns the json string compressed.
    .PARAMETER Indentation
        Optional: The number of spaces (1..1024) to use for indentation. Defaults to 4.
    .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 2
    #>
    [CmdletBinding(DefaultParameterSetName = 'Prettify')]
    Param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string]$Json,

        [Parameter(ParameterSetName = 'Minify')]
        [switch]$Minify,

        [Parameter(ParameterSetName = 'Prettify')]
        [ValidateRange(1, 1024)]
        [int]$Indentation = 4,

        [Parameter(ParameterSetName = 'Prettify')]
        [switch]$AsArray
    )

    if ($PSCmdlet.ParameterSetName -eq 'Minify') {
        return ($Json | ConvertFrom-Json) | ConvertTo-Json -Depth 100 -Compress
    }

    # 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
    $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, AND
            #   - it does not contain a [ or {
            if (($_ -match "[}\]]$regexUnlessQuoted") -and ($_ -notmatch "[\{\[]$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, AND
            #   - it does not contain a ] or }
            if (($_ -match "[\{\[]$regexUnlessQuoted") -and ($_ -notmatch "[}\]]$regexUnlessQuoted")) {
                $indent += $Indentation
            }

            $line
        }

    if ($AsArray) { return $result }
    return $result -Join [Environment]::NewLine
}
widlov
  • 91
  • 1
  • 4
4

If you have the option of using the newer PowerShell Core, the formatting is now fixed. See this SO answer.

oatsoda
  • 2,088
  • 2
  • 26
  • 49
  • Works well. You have to specify the depth each time, and there is no indentation parameter (it uses 2 spaces). Good enough for me though! E.g. Grab any json and plug it into this: `$json | ConvertFrom-Json -Depth 10 | ConvertTo-Json -Depth 10` – Chris Apr 07 '23 at 17:56
4

I wrote the next function to fix the indentation

function FixJsonIndentation ($jsonOutput)
{
    $currentIndent = 0
    $tabSize = 4
    $lines = $jsonOutput.Split([Environment]::NewLine)
    $newString = ""
    foreach ($line in $lines)
    {
        # skip empty line
        if ($line.Trim() -eq "") {
            continue
        }

        # if the line with ], or }, reduce indent
        if ($line -match "[\]\}]+\,?\s*$") {
            $currentIndent -= 1
        }

        # add the line with the right indent
        if ($newString -eq "") {
            $newString = $line
        } else {
            $spaces = ""
            $matchFirstChar = [regex]::Match($line, '[^\s]+')
            
            $totalSpaces = $currentIndent * $tabSize
            if ($totalSpaces -gt 0) {
                $spaces = " " * $totalSpaces
            }
            
            $newString += [Environment]::NewLine + $spaces + $line.Substring($matchFirstChar.Index)
        }

        # if the line with { or [ increase indent
        if ($line -match "[\[\{]+\s*$") {
            $currentIndent += 1
        }
    }

    return $newString
}
Greg
  • 191
  • 1
  • 4
3

Updated the answer from Theo to make it ask for an input in JSON, since I just wanted JSON data to be prettified. Also, removed the math method invocation since I can't use method invocations where I want this to be used.

Thanks so much!

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 Minify
        Optional: Returns the json string compressed.
    .PARAMETER Indentation
        Optional: The number of spaces (1..1024) to use for indentation. Defaults to 4.
    .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 2
    #>
    [CmdletBinding(DefaultParameterSetName = 'Prettify')]
    Param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string]$Json,

        [Parameter(ParameterSetName = 'Minify')]
        [switch]$Minify,

        [Parameter(ParameterSetName = 'Prettify')]
        [ValidateRange(1, 1024)]
        [int]$Indentation = 4,

        [Parameter(ParameterSetName = 'Prettify')]
        [switch]$AsArray
    )

    if ($PSCmdlet.ParameterSetName -eq 'Minify') {
        return ($Json | ConvertFrom-Json) | ConvertTo-Json -Depth 100 -Compress
    }

    # 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
    $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") {

                [int[]] $indentArray = ($indent - $Indentation),0

                if ($indentArray[0] -gt $indentArray[1]) { 
                    $indent = $indentArray[0] 
                    }
                else { 
                    $indent = 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
}


$json = Read-Host "Enter json"

$json | Format-Json

transport
  • 61
  • 9