functions/data/Import-PSFJson.ps1

function Import-PSFJson {
    <#
    .SYNOPSIS
        Imports a json document from file and offers its content as objects.
         
    .DESCRIPTION
        Imports a json document from file and offers its content as objects.
 
        WARNING: The FixData parameter is experimental and may experience breaking changes!
     
    .PARAMETER Path
        Path to the file to import.
        Will evaluate wildcards.
     
    .PARAMETER LiteralPath
        Path to the file to import.
        Will NOT evaluate wildcards.
     
    .PARAMETER Encoding
        The encoding of the file to read.
        Defaults to UTF8.
     
    .PARAMETER AsHashtable
        Return content as hashtable, rather than PSCustomObject
     
    .PARAMETER FixData
        EXPERIMENTAL PARAMETER, MAY SUFFER BREAKING CHANGES
        Attempt to fix broken data from the data processed.
        Assumes the json was originally generated through PowerShell and tries to detect and fix issues that happened during export.
        Most notably: Timestamps no longer being proper timestamps.
     
    .EXAMPLE
        PS C:\> Import-PSFJson .\policies.json
 
        Reads the content of policies.json, parses its structure and returns objects representing its content.
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [PSFFileLax]
        $Path,

        [PSFLiteralPath]
        $LiteralPath,

        [PSFArgumentCompleter('PSFramework-Encoding')]
        [PSFEncoding]
        $Encoding = 'UTF8',

        [switch]
        $AsHashtable,

        [switch]
        $FixData
    )
    begin {
        #region Functions
        function ConvertTo-Hashtable {
            [OutputType([hashtable])]
            [CmdletBinding()]
            param (
                [Parameter(ValueFromPipeline = $true)]
                $InputObject
            )

            begin {
                $jsonTypes = @(
                    'System.String'
                    'System.Int32'
                    'System.Int64'
                    'System.Double'
                    'System.Bool'
                    'System.DateTime'
                )
            }
            process {
                $hashtable = $InputObject | ConvertTo-PSFHashtable
                foreach ($pair in $hashtable.GetEnumerator()) {
                    if ($null -eq $pair.Value) { continue }
                    if ($pair.Value.GetType().FullName -in $jsonTypes) { continue }
                    if ($pair.Value -is [object[]]) {
                        $pair.Value = foreach ($value in $pair.Value) {
                            if ($null -eq $value) { $null; continue }
                            if ($value.GetType().FullName -in $jsonTypes) { $value; continue }
                            if ($value -is [object[]]) { $value; continue } # Accept not resolving double-nested arrays for simplicity
                            ConvertTo-Hashtable -InputObject $value
                        }
                        continue
                    }
                    $pair.Value = ConvertTo-Hashtable -InputObject $pair.Value
                }
                $hashtable
            }
        }
        function Convert-JsonData {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")]
            [CmdletBinding()]
            param (
                [Parameter(ValueFromPipeline = $true)]
                $Data
            )

            begin {
                $jsonTypes = @(
                    'System.String'
                    'System.Int32'
                    'System.Int64'
                    'System.Double'
                    'System.Bool'
                    'System.DateTime'
                )
            }
            process {
                if ($null -eq $Data) { return }

                if ($Data -is [object[]]) {
                    ,@(ConvertFrom-JsonArray -Data $Data)
                    return
                }

                if ($Data -is [hashtable]) { $properties = $Data.Keys }
                else { $properties = $Data.PSObject.Properties.Name }

                foreach ($property in $properties) {
                    $item = $Data.$property
                    if ($null -eq $item) { continue }
                    if ($item -is [string] -and $item -match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{7}\+\d{2}:\d{2}$') {
                        $Data.$Property = $item -as [datetime]
                        continue
                    }
                    if ($item.GetType().FullName -in $jsonTypes) { continue }
                    if ($item -is [object[]]) {
                        $Data.$property = @(ConvertFrom-JsonArray -Data $item)
                        continue
                    }
                    
                    # DateTime vNext
                    if (
                        $($item.PSObject.Properties).Count -eq 3 -and
                        $item.PSObject.Properties.Name -contains 'value' -and
                        $item.PSObject.Properties.Name -contains 'DisplayHint' -and
                        $item.PSObject.Properties.Name -contains 'DateTime' -and
                        $item.value -is [datetime]
                    ) {
                        $Data.$property = $item.value
                        continue
                    }

                    $Data.$property = Convert-JsonData -Data $item
                }

                $Data
            }
        }
        function ConvertFrom-JsonArray {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")]
            [CmdletBinding()]
            param (
                $Data
            )

            $jsonTypes = @(
                'System.String'
                'System.Int32'
                'System.Double'
                'System.Bool'
                'System.DateTime'
            )

            foreach ($item in $Data) {
                if ($null -eq $item) { $null; continue }
                if ($item -is [string] -and $item -match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{7}\+\d{2}:\d{2}$') {
                    $item -as [datetime]
                    continue
                }
                if ($item.GetType().FullName -in $jsonTypes) { $item; continue }
                if ($item -is [object[]]) { ,@(ConvertFrom-JsonArray -Data $item); continue }
                if (
                    $item.PSObject.Properties.Count -eq 3 -and
                    $item.PSObject.Properties.Name -contains 'value' -and
                    $item.PSObject.Properties.Name -contains 'DisplayHint' -and
                    $item.PSObject.Properties.Name -contains 'DateTime' -and
                    $item.value -is [datetime]
                ) {
                    $item.value
                    continue
                }
                Convert-JsonData -Data $item
            }
        }
        #endregion Functions
    }
    process {
        foreach ($entry in $Path.FailedInput) {
            Write-Error "Could not resolve as file: $entry"
        }

        foreach ($filePath in $Path + $LiteralPath) {
            $content = Get-PSFFileContent -LiteralPath $filePath -Encoding $Encoding
            if (-not $AsHashtable) { $data = $content | ConvertFrom-Json }
            else {
                if ($PSVersionTable.PSVersion.Major -gt 5) { $data = $content | ConvertFrom-Json -AsHashtable }
                else { $data = $content | ConvertFrom-Json | ConvertTo-Hashtable }
            }

            if (-not $FixData) {
                $data
                continue
            }

            # Fix Data (expensive)
            Convert-JsonData -Data $data
        }
    }
}