首页 > 解决方案 > 如何将powershell中的对象序列化为json并在PS桌面和核心中获得相同的结果?

问题描述

序言

事实证明,就我而言,了解对象的来源很重要——它是来自 REST API 响应的 JSON 有效负载。不幸的是,JSON -> 对象转换在 PS 桌面和 PS 核心上产生不同的结果。在桌面上,数字被反序列化为Int32类型,但在核心 - 到Int64类型。由此得出我不能使用Export-CliXml,因为对象的二进制布局不同。

主要问题

我有一个单元测试需要将实际结果与预期结果进行比较。预期的结果保存在一个json文件中,所以过程是:

  1. 将实际结果转换为json字符串
  2. 将预期结果从磁盘读取到字符串
  3. 比较实际和预期的字符串

不幸的是,这个方案不起作用,因为 PS 桌面ConvertTo-Json和 PS 核心ConvertTo-Json不会产生相同的结果。因此,如果预期结果保存在桌面上并且测试在核心上运行 - 繁荣,失败。反之亦然。

一种方法是保留两个版本的 jsons。另一种方法是使用库来创建 json。

首先,我尝试了Newtonsoft-Json powershell 模块,但它不起作用。我认为问题在于,无论我们使用什么 C# 库,它都必须了解PSCustomObject和相似并特别对待它们。因此,我们不能只使用任何 C# JSON 库。

在这一点上,我只剩下两个 json - 每个 PS 版本一个,这有点可悲。

有更好的选择吗?

编辑 1

我想我总是可以读取 json,转换为对象,然后再次返回 json。这太糟糕了。

编辑 2

我尝试使用ConvertTo-Json -Compress. 这消除了间距的差异,但问题是由于某种原因桌面版本将所有非字符转换为\u000...表示形式。核心版本不这样做。

请注意:

桌面

C:\> @{ x = "'a'" } |ConvertTo-Json -Compress
{"x":"\u0027a\u0027"}
C:\>

C:\>  @{ x = "'a'" } |ConvertTo-Json -Compress
{"x":"'a'"}
C:\>

现在核心版本有 flag -EscapeHandling,所以:

C:\>  @{ x = "'a'" } |ConvertTo-Json -Compress -EscapeHandling EscapeHtml
{"x":"\u0027a\u0027"}
C:\>

答对了!结果相同。但是现在这个代码不能在没有这个标志的桌面版本上运行。需要更多的按摩。我会检查这是否是唯一的问题。

编辑 3

如果不进行昂贵的后期处理,就不可能调和核心版本和桌面版本之间的差异。请注意:

桌面

C:\> @{ x = '"a"';y = "'b'" } |ConvertTo-Json -Compress
{"y":"\u0027b\u0027","x":"\"a\""}
C:\>

C:\> @{ x = '"a"';y = "'b'" } |ConvertTo-Json -Compress -EscapeHandling EscapeHtml
{"y":"\u0027b\u0027","x":"\u0022a\u0022"}
C:\> @{ x = '"a"';y = "'b'" } |ConvertTo-Json -Compress
{"y":"'b'","x":"\"a\""}
C:\>

关于如何挽救 json 方法的任何建议?

编辑 4

由于 PS 版本之间的差异,该Export-CliXml方法也不起作用。

桌面

C:\> ('{a:1}' | ConvertFrom-Json).a.gettype()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType


C:\>

C:\> ('{a:1}' | ConvertFrom-Json).a.gettype()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int64                                    System.ValueType

C:\>

因此,相同的 JSON 使用不同的数字类型表示 -Int32在桌面和Int64核心中。这使得使用Export-CliXml.

除非我错过了什么。

我相信没有其他选择,但是做双重转换 - json -> object -> json然后我将在同一个 PS 版本上创建两个 json。这很糟糕。

标签: jsonpowershell

解决方案


  • 原始 JSON转换时,使用第三方Newtonsoft.Json PowerShell wrapperConvertFrom-JsonNewtonsoftcmdlet - 这应该确保跨版本兼容性(内置ConvertFrom-Json保证跨 PowerShell 版本,因为 Windows PowerShell 使用自定义解析器,而PowerShell [Core] v6+ 使用 Newtonsoft.json 至少到 v7.1,尽管即将迁移到新的(ish).NET Core System.Text.JsonAPI)。

    • 重要ConvertFrom-JsonNewtonsoft返回(数组)嵌套有序哈希表[ordered] @{ ... }, ),与内置输出System.Collections.Specialized.OrderedDictionary的嵌套 图不同。同样,只需要(数组)哈希表(字典)作为输入,尤其是支持实例,正如您自己了解到的那样[pscustomobject]ConvertFrom-JsonConvertTo-JsonNewtonsoft[pscustomobject]

      • 警告:在撰写本文时,包装器模块最后一次更新是在 2019 年 5 月,并且底层捆绑Newtonsoft.Json.dll程序集的版本相当旧(8.0,而在撰写本文时12.0最新版本)。请参阅模块的源代码
    • 请注意,为了手动解析从 RESTful Web 服务获得的 JSON,您不能使用Invoke-RestMethod,因为它会隐式解析并返回[pscustomobject]对象图。相反,使用Invoke-WebRequest和访问返回的响应的.Content属性。

  • 转换适合存储在磁盘上的格式时,您有两种选择:

    • (A)如果您确实需要序列化格式也为 JSON,则必须将所有[pscustomobject]图形转换为(有序)哈希表,然后再将它们传递给ConvertTo-JsonNewtonsoft.

      • 请参阅下面的功能ConvertTo-OrderedHashTable,它就是这样做的。
    • (B)如果特定的序列化格式不重要,即如果所有重要的是格式在 PowerShell 版本之间是相同的以便比较,则不需要额外的工作:使用内置的Export-Clixmlcmdlet,它可以处理任何键入并生成PowerShell 的原生、基于 XML 的序列化格式,称为 CLIXML(特别是在 PowerShell 远程处理中使用),它应该是跨版本兼容的(至少在 Windows PowerShell 端与 v5.1 和从 PowerShell [Core] v7 开始.1,两者都使用相同版本的序列化协议,1.1.0.1如 ) 所报告的$PSVersionTable.SerializationVersion

      • 虽然您可以使用 将此类持久文件重新转换为对象,但反序列化时类型保真度Import-Clixml潜在损失使得比较序列化(CLIXML) 表示是可取的。

      • 另请注意,从 PowerShell v7.1 开始,没有基于 cmdlet 的方法来创建内存中CLIXML 表示,因此您现在必须直接使用 PowerShell API System.Management.Automation.PSSerializer.Serialize:. Import-CliXml但是,以/ cmdletExport-CliXml的形式为ConvertFrom-CliXml/提供内存中的对应项ConvertTo-CliXml已被批准为未来的增强功能


Re (A): 这是函数ConvertTo-OrderedHashtable,它将(可能嵌套的)[pscustomobject]对象转换为有序哈希表,同时传递其他类型,因此您应该能够简单地将其插入管道中,如下所示:

# CAVEAT: ConvertTo-JsonNewtonSoft only accepts a *single* input object.
[pscustomobject] @{ foo = 1 }, [pscustomobject] @{ foo = 2 } | 
  ConvertTo-OrderedHashtable |
    ForEach-Object { ConvertTo-JsonNewtonSoft $_ }
function ConvertTo-OrderedHashtable {
<#
.SYNOPSIS
Converts custom objects to ordered hashtables.

.DESCRIPTION
Converts PowerShell custom objects (instances of [pscustomobject]) to
ordered hashtables (instances of [System.Collections.Specialized.OrderedDictionary]),
which is useful for to-JSON serialization via the Newtonsoft.JSON library.

Note: 
 * Custom objects are processed recursively.
 * Any scalar non-custom objects are passed through as-is.
 * Any (non-dictionary) collections in property values are converted to 
  [object[]] arrays.

.EXAMPLE
1, [pscustomobject] @{ foo = [pscustomobject] @{ bar = 'none' }; other = 2 } | ConvertTo-OrderedHashtable

Passes integer 1 through, and converts the custom object to a nested ordered
hashtable.
#>
  [CmdletBinding()]
  param(
    [Parameter(ValueFromPipeline)] $InputObject
  )

  begin {

    # Recursive helper function
    function convert($obj) {
      if ($obj -is [System.Management.Automation.PSCustomObject]) {
        # a custom object: recurse on its properties
        $oht = [ordered] @{ }
        foreach ($prop in $obj.psobject.Properties) {
          $oht.Add($prop.Name, (convert $prop.Value))
        }
        return $oht
      }
      elseif ($obj -isnot [string] -and $obj -is [System.Collections.IEnumerable] -and $obj -isnot [System.Collections.IDictionary]) {
        # A collection of sorts (other than a string or dictionary (hash table)), recurse on its elements.
        return @(foreach ($el in $obj) { convert $el })
      }
      else { 
        # a non-custom object, including .NET primitives and strings: use as-is.
        return $obj
      }
    }

  }

  process {

    convert $InputObject

  }

}

Re (B):该方法的演示Export-CliXml(您可以从任一 PS 版本运行此代码):

$sb = {

  Install-Module -Scope CurrentUser Newtonsoft.json
  if (-not $IsCoreClr) { 
    # Workaround for PS Core's $env:PSModulePath overriding WinPS'
    Import-Module $HOME\Documents\WindowsPowerShell\Modules\newtonsoft.json
  }
  @'
  {
      "results": {
          "users": [
              {
                  "userId": 1,
                  "emailAddress": "jane.doe@example.com",
                  "date": "2020-10-05T08:08:43.743741-04:00",
                  "attributes": {
                      "height": 165,
                      "weight": 60
                  }
              },
              {
                  "userId": 2,
                  "emailAddress": "john.doe@example.com",
                  "date": "2020-10-06T08:08:43.743741-04:00",
                  "attributes": {
                      "height": 180,
                      "weight": 72
                  }
              }
          ]
      }
  }
'@ | ConvertFrom-JsonNewtonsoft | Export-CliXml "temp-$($PSVersionTable.PSEdition).xml"
  
}
  
# Execute the script block in both editions

Write-Verbose -vb 'Running in Windows PowerShell...'
powershell -noprofile $sb

Write-Verbose -vb 'Running in PowerShell Core...'
pwsh -noprofile $sb
  
# Compare the resulting CLIXML files.
Write-Verbose -vb "Comparing the resulting files: This should produce NO output,`n         indicating that the files have identical content."
Compare-Object (Get-Content 'temp-Core.xml') (Get-Content 'temp-Desktop.xml')

Write-Verbose -vb 'Cleaning up...'
Remove-Item 'temp-Core.xml', 'temp-Desktop.xml'

您应该看到以下详细输出:

VERBOSE: Running in Windows PowerShell...
VERBOSE: Running in PowerShell Core...
VERBOSE: Comparing the resulting files: This should produce NO output, 
         indicating that the files have identical content.
VERBOSE: Cleaning up...

推荐阅读