Continuing on from earlier, after I discovered that FireFox includes far more bookmark information in its native format (JSON) I wanted to create a parser for that instead. Normally PowerShell provides you two simple ways of doing this: ConvertFrom-Json and ConvertTo-Json.

Unfortunately ConvertFrom-Json is a weak link in this chain as it does not allow you to define the maximum length of the input string or recursion level, and can easily crash with an error:

ConvertFrom-Json : Error during serialization or deserialization using the JSON JavaScriptSerializer. The length of the string exceeds the value set on the maxJsonLength property.
Parameter name: input
At line:1 char:1
+ ConvertFrom-Json (Get-Content $jsonFileName)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [ConvertFrom-Json], ArgumentException
    + FullyQualifiedErrorId : System.ArgumentException,Microsoft.PowerShell.Commands.ConvertFromJsonCommand</font>

There is an alternative which is deserializing the JSON using the .NET class JavaScriptSerializer directly, though you are then locked into also using this same class to serialize it again later; the reason for this is that both conversions work using arrays and dictionaries. By way of comparison the ConvertTo-Json and ConvertFrom-Json functions use arrays and PowerShell custom objects instead.

So I wrote a function to convert all the dictionaries from the JavaScriptSerializer to PowerShell custom objects, so you could load using the .NET deserializer but then serialize back with the PowerShell function. I'm a little funny like that:

function Iterate-Tree($jsonTree) {
    $result = @()

    # Go through each node in the tree
    foreach ($node in $jsonTree) {

        # For each node we need to set up its keys/properties/fields
        $nodeHash = @{}
        foreach ($property in $node.Keys) {
            # If a field is a set (either a dictionary or array - both used by the deserializer) we will need to iterate it
            if ($node[$property] -is [System.Collections.Generic.Dictionary[String, Object]] -or $node[$property] -is [Object[]]) {
                # This assignment is important as it forces single result sets to be wrapped in an array, which is required
                $inner = @()
                $inner += Iterate-Tree $node[$property]

                $nodeHash.Add($property, $inner)
            } else {
                $nodeHash.Add($property, $node[$property])
            }
        }

        # Create a custom object from the hash table so it matches the original. It must be a PSCustomObject
        # because the serializer (later) requires that and not a PSObject or HashTable.
        $result += [PSCustomObject] $nodeHash
    }

    return $result
}

Add-Type -AssemblyName System.Web.Extensions
$javaScriptSerializer = New-Object System.Web.Script.Serialization.JavaScriptSerializer
$javaScriptSerializer.MaxJsonLength = [System.Int32]::MaxValue
$javaScriptSerializer.RecursionLimit = 99

$jsonFileName = "Y:DesktopBookmarks.json"
$jsonContent = Get-Content $jsonFileName
$jsonTree = $javaScriptSerializer.DeserializeObject($jsonContent)
$jsonTree = Iterate-Tree $jsonTree
# The -Compress option is important, and works around conversion bugs revolving around single double-quotes
ConvertTo-Json $jsonTree -Depth 99 -Compress | Set-Content (Get-ChildItem $jsonFileName | %{ Join-Path $_.DirectoryName "$($_.BaseName)_Fixed$($_.Extension)" } )

However after all of this work, it turned out I did not need to do any of this.

Firstly because my initial size issue was caused by a bad file that had doubled-up contents; re-exporting produced a file half the size which did not trigger any problem in ConvertFrom-Json at all.

Secondly I was concerned with manipulating the JavaScriptSerializer version of the object because of the dictionaries it used. At the time I could not work out how to create equivalent objects in PowerShell. I later worked it out with:

$jsonTree.GetType() | Select-Object -Property UnderlyingSystemType
# System.Collections.Generic.Dictionary`2[String,Object]
# Leading to...
New-Object "System.Collections.Generic.Dictionary[String, Object]"

Which is easy to do, and easy to manipulate; no need to use PowerShell custom objects at all.