Converting JSON to a hashtable


Table of Contents

What’s the problem?

The ConvertFrom-Json and ConvertTo-Json cmdlets are great. JSON is a pretty common standard that isn’t going anywhere, and the ability to work with JSON data as native PowerShell objects provides first-class support for this format in PowerShell. It’s far simpler for PS to use JSON than to use XML.

However, there are times when the PSCustomObject returned by ConvertFrom-Json is less than ideal, and it would be more helpful to use a hashtable instead:

Strict mode and missing properties

When using Strict mode, checking whether the JSON contained a key uses this fairly tedious snippet:

    $obj = $json | ConvertFrom-Json
    if ($obj.PSObject.Properties.Name -contains 'MyCustomKey') {
        # ...
    }

Hashtables make this far easier:

    $hash = @{ a = 1; b = 2;}
    if ($hash.ContainsKey('MyCustomKey')) {
        # ...
    }

    # Also valid
    if ($hash['MyCustomKey']) {
        # ...
    }

Extending and combining objects

PSCustomObjects have to be extended using Add-Member:

    $obj | Add-Member -MemberType NoteProperty -Name 'MyCustomKey' -Value 'foo'

Hashtables can have additional properties added implicitly, even if the key doesn’t exist:

    $hash['MyCustomKey'] = 'foo'

    # More formal
    $hash.Add('MyCustomKey', 'foo')

Hashtables can also be combined easily:

    $hash1 = @{a = 1}
    $hash2 = @{b = 2}

    $hash3 = $hash1 + $hash2

Is there a convenient way for us to create a hashtable from JSON?

That’s a Wrap

Actually, it turns out that this is a pretty trivial process with the lesser-known JavaScriptSerializer class. (I believe this was added in .NET 4.5, but if I’m incorrect, feel free to let me know.) This assembly isn’t loaded by default in a PowerShell session, but thanks to Add-Type, loading an assembly is trivial (notice that the MSDN page tells us exactly what DLL file to reference). Thanks to Kevin Marquette’s article on hashtables for pointing me in this direction!

This is only half of the problem, though. We could write a function to wrap this class, but then any code where we need to reference our JSON-to-hashtable logic would need to include or reference that function. We’d need to do a find/replace to change all references of ConvertFrom-Json to our custom function name.

Instead, PowerShell’s command precedence allows us to define a custom function that “overrides” a built-in cmdlet. PowerShell resolves commands in this order:

  1. Aliases
  2. Functions
  3. Native cmdlets
  4. Binaries (anything in the PATH variable)

Because of this, we can create a custom function and name it ConvertFrom-Json, and it will take precedence over the native cmdlet (a function takes precedence over a cmdlet). This is the same reason typing dir in PowerShell resolves to an alias to Get-ChildItem before it resolves to dir.exe - an alias takes precedence over a binary. This means when we load a custom function called ConvertFrom-Json, it will magically be used by default anywhere we call ConvertFrom-Json in that PowerShell session.

You can create a wrapper function yourself line-by-line if you’d like, but I found Jeff Hicks’ Copy-Command function gave me a pretty good starting point:

    PS> Copy-Command -Command 'ConvertFrom-Json' -UseForwardHelp -IncludeDynamic | Out-File C:\PowerShell\Scripts\ConvertFrom-Json.ps1

The -UseForwardHelp switch generates some metadata that points Get-Help ConvertFrom-Json to the cmdlet itself, rather than our function. This means we don’t need to mess with comment-based help here. (We don’t have help for the new parameter, -As, but that’s what this article is for, right?)

The -IncludeDynamic switch causes the Copy-Command function to copy any dynamic parameter blocks from the cmdlet to our new wrapper function. As it turns out, this doesn’t matter in this case, but it doesn’t hurt - and it’s helpful if the cmdlet you wrap does include dynamic parameters.

The Code

Here’s the full code for my custom wrapper function around ConvertFrom-Json:

    function ConvertFrom-Json {
        <#
        .ForwardHelpTargetName Microsoft.PowerShell.Utility\ConvertFrom-Json
        .ForwardHelpCategory Cmdlet
        #>
        [CmdletBinding(HelpUri = 'http://go.microsoft.com/fwlink/?LinkID=217031', RemotingCapability = 'None')]
        param(
            [Parameter(Mandatory = $true,
                Position = 0,
                ValueFromPipeline = $true)]
            [AllowEmptyString()]
            [String] $InputObject,

            [Parameter()]
            [ValidateSet('Object', 'Hashtable')]
            [String] $As = 'Object'
        )

        begin {
            Write-Debug "Beginning $($MyInvocation.Mycommand)"
            Write-Debug "Bound parameters:`n$($PSBoundParameters | out-string)"

            try {
                # Use this class to perform the deserialization:
                # https://msdn.microsoft.com/en-us/library/system.web.script.serialization.javascriptserializer(v=vs.110).aspx
                Add-Type -AssemblyName "System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" -ErrorAction Stop
            }
            catch {
                throw "Unable to locate the System.Web.Extensions namespace from System.Web.Extensions.dll. Are you using .NET 4.5 or greater?"
            }

            $jsSerializer = New-Object -TypeName System.Web.Script.Serialization.JavaScriptSerializer
        }

        process {
            switch ($As) {
                'Hashtable' {
                    $jsSerializer.Deserialize($InputObject, 'Hashtable')
                }
                default {
                    # If we don't know what to do, use the native cmdlet.
                    # This should also catch the -As Object case.

                    # Remove -As since the native cmdlet doesn't understand it
                    if ($PSBoundParameters.ContainsKey('As')) {
                        $PSBoundParameters.Remove('As')
                    }

                    Write-Debug "Running native ConvertFrom-Json with parameters:`n$($PSBoundParameters | Out-String)"
                    Microsoft.PowerShell.Utility\ConvertFrom-Json @PSBoundParameters
                }
            }
        }

        end {
            $jsSerializer = $null
            Write-Debug "Completed $($MyInvocation.Mycommand)"
        }

    }

Example Usage

    PS> $json = @'
    {
        "cat":  "meow",
        "dog":  "woof",
        "programmer":  "derp"
    }
    '@

    PS> $json | ConvertFrom-Json

    cat  dog  programmer
    ---  ---  ----------
    meow woof derp

    PS> PS> $json | ConvertFrom-Json -As Hashtable

    Name                           Value
    ----                           -----
    dog                            woof
    programmer                     derp
    cat                            meow

Notice that the default behavior doesn’t change - if we don’t specify what we want to do with the JSON, it will be converted to an object as usual. This prevents any disruption to code that uses the built-in cmdlet and expects an object in response.

Conclusion

It might be worthwhile to wrap the JSON.NET library rather than the .NET class I referenced, since even MSDN has a note that serialization should be done with JSON.NET rather than that class. Since that would introduce a dependent library, though, it would be a candidate for a more full-featured module instead of a quick wrapper function. In fact, PowerShell Core seems to be referencing this library already!

Finally, if you plan to use this wrapper in code that gets pushed anywhere other than your local machine, it would be prudent to include the wrapper function as a private function in your module. That way, you don’t have to assume that the other system has this function available in its PowerShell profile.

I hope this is helpful!