遇到特殊需求,PowerShell 產生 JSON 時需將中文字元轉成 UNC (Unicode Character Name,例如 "\u9ED1\u6697\u57F7\u884C\u7DD2"),之前處理過 ASP.NET Core JSON 中文編碼問題,大致有概念,用 Newtonsoft Json.NET 指定 JsonSerializerSettings.StringEscapeHandling = StringEscapeHandling.EscapeNonAscii 應可搞定。

不過,序列化對象是自訂 PSCustomObject 陣列,用 ConvertTo-Json 能正常轉換,改成 Newtonsoft.Json.Convert.SerializeObject() 卻變成奇怪的 XML。

講序列化問題前,先幫不熟 PSCustomObject 的同學做個補充。在 PowerShell 裡,用 @{ Prop1 = ...; Prop2 = ... } 就能建立自訂物件並動態指定屬性,例如:

$custObj = @{
    Prop1 = "One"
    Prop2 = "Two"
}
$custObj.Prop3 = "Three"
$custObj | ConvertTo-Json
$custObj | ConvertTo-Csv

但有兩個問題,第一是 ConvertTo-Json 可順利轉成 JSON,但卻沒有依循屬性宣告順序,原因是 $custObj 骨子裡是 Hashtable,這衍生第二個問題,ConvertTo-Csv 或 Format-Table 時,不會以 Prop1、Prop2、Prop3 屬性欄位方式呈現。

改用 PSCustomObject 方式,除了加屬性需改用 Add-Member 麻煩些,其餘行為更符合我們對自訂物件的期待。

$custObj = [PSCustomObject]@{
    Prop1 = "One"
    Prop2 = "Two"
}
$custObj | Add-Member -MemberType NoteProperty -Name "Prop3" -Value "Three"
$custObj | ConvertTo-Json
$custObj | ConvertTo-Csv

下面範例展示 Hashtable、PSCustomObject 在 Format-Table、Where-Object、Select-Object 處理上的異同:

$rnd = New-Object System.Random(123)
$players = 1..10 | ForEach-Object {
    # Hashtable
    @{
        Name = "Player$_"
        Score = $rnd.Next(256)
    }
}
Write-Host 'Hashtable 陣列接 Format-Table' -ForegroundColor Yellow
$players | Format-Table
Write-Host '篩選 Score > 127 並輸出 Name 字串陣列' -ForegroundColor Yellow
$players | Where-Object { $_.Score -gt 127 } | ForEach-Object { $_.Name }

$rnd = New-Object System.Random(123)
$players = 1..10 | ForEach-Object {
    [PSCustomObject]@{
        Name = "Player$_"
        Score = $rnd.Next(256)
    }
}
Write-Host 'PSCustomObject 陣列接 Format-Table' -ForegroundColor Yellow
$players | Format-Table
$greaterThan127 = $players | Where-Object { $_.Score -gt 127 } 
Write-Host '篩選 Score > 127 並輸出含 Name 屬性的物件陣列' -ForegroundColor Yellow
$greaterThan127 | Select-Object Name | Format-Table
Write-Host '篩選 Score > 127 並輸出 Name 字串陣列' -ForegroundColor Yellow
$greaterThan127 | Select-Object -ExpandProperty Name

MS Docs 有篇您想知道有關 PSCustomObject 的一切,名符其實,整理十分完整,值得一讀。

回到這次的問題上,PSCustomObject 物件陣列用 ConvertTo-Json 轉換很 OK,為了將中文字元顯示為 UCN 改用 [Newtonsoft.Json.JsonConvert]::SerializeObject() 卻得到奇怪的 CliXml XML 內容。

Add-Type -Path "$PSScriptRoot\Newtonsoft.Json.dll"

$list = @()
$list += [PSCustomObject]@{
    Id   = 'A001'
    Name = 'Jeffrey'
}
$list += [PSCustomObject]@{
    Id   = 'A002'
    Name = '黑暗執行緒'
}

$list | ConvertTo-Json

function ConvertToJsonByJsonNet($object) {

    $jsonSettings = New-Object Newtonsoft.Json.JsonSerializerSettings
    $jsonSettings.StringEscapeHandling = [Newtonsoft.Json.StringEscapeHandling]::EscapeNonAscii
    $jsonSettings.Formatting = [Newtonsoft.Json.Formatting]::Indented
    [Newtonsoft.Json.JsonConvert]::SerializeObject($object, $jsonSettings)
}

ConvertToJsonByJsonNet($list)

研究了一下,所謂 Clixml 是 PowerShell 物件內部採用的 XML 物件表示格式,PowerShell 還有提供 Export-ClixmlImport-Clixml 等匯出匯入工具。而從 .NET 角度存取 PSCustomObject,看到的就只有 Clixml 屬性。

我找到幾種解法:

  1. 將 PSCustomObject 轉成 Hashtable 參考
  2. 使用 ConvertTo-NewtonsoftJson.ps1 等現成函式 參考
  3. 先 ConvertTo-Json 轉成 JSON 再用 JSON.NET 轉成一般物件:
     $json = $list | ConvertTo-Json
     $fixed = [Newtonsoft.Json.JsonConvert]::DeserializeObject($json)
     ConvertToJsonByJsonNet($fixed)
    

用 ConvertTo-Json 轉 JSON 再 [Newtonsoft.Json.JsonConvert]::DeserializeObject() 感覺最簡便,最後用它搞定。

Tips of how to serializing PSCustomObject with Json.NET.


Comments

Be the first to post a comment

Post a comment