Lazy Diary @ Hatena Blog

PowerShell / Java / miscellaneous things about software development, Tips & Gochas. CC BY-SA 4.0/Apache License 2.0

Convert deeply nested hash or array to JSON with ConvertTo-Json

Context

You can read a JSON file like below with ConvertFrom-Json, and write with ConvertTo-Json properly.

PS > Get-Content ./foo.json
{
  "outerHash": {
    "innerHash": {
      "key": "value"
    }
  }
}
PS > Get-Content ./foo.json | ConvertFrom-Json | ConvertTo-Json
{
  "outerHash": {
    "innerHash": {
      "key": "value"
    }
  }
}

Problem

You also can read a JSON file like below with ConvertFrom-Json, but cannot write with ConvertTo-Json properly. ConvertTo-Json write a hash in array in hash as "@{key=value}" style string.

PS > Get-Content ./bar.json
{
  "outerHash": {
    "arrayInHash": [
      {
        "key": "value"
      }
    ]
  }
}
PS > Get-Content ./bar.json | ConvertFrom-Json | ConvertTo-Json
{
  "outerHash": {
    "arrayInHash": [
      "@{key=value}"
    ]
  }
}

In this example, the result of ConvertTo-Json should be identical the result of Get-Content ./bar.json.

Reason

ConvertTo-Json has a option -Depth, and its default value is 2. This option is used when convert nested object to JSON.

Solution

Pass the appropriate value to -Depth option according to your object graph.

PS > Get-Content ./bar.json | ConvertFrom-Json | ConvertTo-Json -Depth 3
{
  "outerHash": {
    "arrayInHash": [
      {
        "key": "value"
      }
    ]
  }
}

Convert an array to CSV with PowerShell

Problem

You cannot convert an array with just pipeline the array to ConvertTo-Csv.

PS > $array = ("a", "b", "c", "a", "d")
PS > $array | ConvertTo-Csv
"Length"
"1"
"1"
"1"
"1"
"1"

... or just passing the array to ConvertTo-Csv.

PS > ConvertTo-Csv $array
"Length","LongLength","Rank","SyncRoot","IsReadOnly","IsFixedSize","IsSynchronized","Count"
"5","5","1","System.Object[]","False","True","False","5"

An array in object also will produce undesired result.

PS > $obj = New-Object PSObject
PS > $obj | Add-Member -MemberType NoteProperty -Name "CSV" -Value $array
PS > $obj | ConvertTo-Csv
"CSV"
"System.Object[]"
PS > ConvertTo-Csv $obj
"CSV"
"System.Object[]"

Reason

It seems by design.

Solution

You should use Add-Member with an loop and dummy property names.

PS > $obj = New-Object PSObject
PS > for ($i=0; $i -lt $array.Length; $i++) { $obj | Add-Member -MemberType NoteProperty -Name $i -Value $array[$i] }
PS > $obj | ConvertTo-Csv -NoTypeInformation | Select-Object -Skip 1
"a","b","c","a","d"

XmlNode.SelectNodes() always returns List in PowerShell 2.0

Problem:

In PowerShell, by using XML DOM API in .NET, you can access to a child element in XML as a ordinary property.

PS > $xml = New-Object System.Xml.XmlDocument
PS > $xml.LoadXml('<a><b id="1">foo</b></a>')
PS > $xml.SelectNodes('//a').b

id                                                            #text
--                                                            -----
1                                                             foo

But in PowerShell 2.0, the same script returns nothing ($null).

PS > $xml = New-Object System.Xml.XmlDocument
PS > $xml.LoadXml('<a><b id="1">foo</b></a>')
PS > $xml.SelectNodes('//a').b

Reason:

It seems to be by design, or caused by some unintentional change in implementation of PowerShell.

  • In PowerShell 4.0 or later, the result of SelectNodes() will be XmlElement if there is only one element in the result.
  • In PowerShell 2.0, the result of SelectNodes() will be always an list of XmlElement.

Solution:

You should always get child nodes through ForEach-Object. The script below does not return $null in PowerShell 2.0 and 4.0 or later.

PS > $xml = New-Object System.Xml.XmlDocument
PS > $xml.LoadXml('<a><b id="1">foo</b></a>')
PS > $xml.SelectNodes('//a') | ForEach-Object { $_ }

b
-
b

How to get a #text in XML even if the tag doesn't have attributes

Background:

In PowerShell (even in C# or VB.NET?), you can get a body of the tag (text content) with '#text#' property.

> $xml = New-Object System.Xml.XmlDocument
> $xml.LoadXml('<a><b id="1">foo</b></a>')
> $xml.SelectNodes('//a').b.'#text'
foo

Problem:

If a tag has no attributes, you cannot get text content with '#text#' property.

> $xml = New-Object System.Xml.XmlDocument
> $xml.LoadXml('<a><b>foo</b></a>')
> $xml.SelectNodes('//a').b.'#text'
(nothing shown)

Reason:

The type of $xml.SelectNodes('//a').b will be XmlElement when <b> has attributes. On the other hand, it will be String when <b> has no attributes.

> $xml = New-Object System.Xml.XmlDocument
> $xml.LoadXml('<a><b id="1">foo</b></a>')
> $xml.SelectNodes('//a').b.GetType().Name
XmlElement
> $xml.LoadXml('<a><b>foo</b></a>')
> $xml.SelectNodes('//a').b.GetType().Name
String

Solution:

Use XPath method rather than property on DOM object. SelectSingleNode() will alrays return XmlElement and you can use #text property.

> $xml = New-Object System.Xml.XmlDocument
> $xml.LoadXml('<a><b id="1">foo</b></a>')
> $xml.SelectNodes('//a').SelectSingleNode('//b').'#text'
foo
> $xml.LoadXml('<a><b>foo</b></a>')
> $xml.SelectNodes('//a').SelectSingleNode('//b').'#text'
foo

Who does recommend to encrypt the attachments in email

In Japan, so many companies have their own security policy like "When you send email with attachments, you must zip all the attachments with password, and send the password in another email". Some say this policy is pointless, but on the other hand, some standards recommend to do this encryption.

Who recommends to encrypt the attachments in email