Creating complex JSON with Powershell

by Matthew Hart on June 08, 2019

How do I create a JSON file with Powershell?
How do I create a JSON array with Powershell?
How do I create a JSON List using Powershell?
How can I create a JSON array with a List inside with Powershell and vice-versa?

This post will serve as a reference to show how to make complex JSON structures with Powershell, as things can get messy when you are trying to figure out how to use a list or an Array structure and meld them together to make your desired JSON.

How to write to a JSON file

First the basics, here is how you can write out your data to a JSON file:

$yourData | ConvertTo-Json -Depth 10 | Out-File ".\myJsonFile.json"

What does this mean? First we get our variable $yourData which contains your data whether its a list or a hashtable or a mix of each. We then pipe this variable into ConvertTo-Json, we then need to specify the Depth. By default the depth is 2, depth refers to the number of levels powershell can write into the JSON. If your JSON has a lot of nested arrays and lists, increase the depth to prevent powershell from ignoring deeper elements in your data.

Lastly we pipe out the contents to a JSON file that will be created if it doesn’t exist, using the Out-File command. Adding .\ in front of the file name will create the JSON file in the same directory that the script is running in.

Constructing our JSON

Defining a JSON List

Below is the full code snippet to make a basic list in JSON:

$jsonBase = @{}
$list = New-Object System.Collections.ArrayList
$list.Add("Foo")
$list.Add("Bar")
$jsonBase.Add("Data",$list)
$jsonBase | ConvertTo-Json -Depth 10 | Out-File ".\write-list.json"

This produces the following JSON:

{
  "Data": [
    "Foo",
    "Bar"
  ]
}

How does this work? First to begin our JSON with the curly brackets we need to define a hashtable, we can do this by defining a variable with @{}. What happens if I don't use an empty hashtable at the start? Depending on how big your list is, if there is only one element it will just write in that single element without any square brackets. I circumvent this by nesting the list into a hashtable.

Next we define a list using New-Object System.Collections.ArrayList.

We then add our elements by calling .Add() on the variable defined as a List.

Lastly, we add our List to our Hashtable (curly braces) by using the .Add() method however in hashtables we need to additionally define a key in the first argument .Add(key,object). Which is then piped out to a json file.

Defining a JSON Array

Below is the snippet to make a basic JSON Array:

$jsonBase = @{}

$array = @{}

$data = @{"Name"="Matt";"Colour"="Black";}

$array.Add("Person",$data)

$jsonBase.Add("Data",$array)
$jsonBase | ConvertTo-Json -Depth 10 | Out-File ".\write-array.json"

This produces the following JSON:

{
  "Data": {
    "Person": {
      "Name": "Matt",
      "Colour": "Black"
    }
  }
}

Combining Lists and Arrays

This next set of examples will combine what we have learned and mix our data structures within the same file.

$jsonBase = @{}
$list = New-Object System.Collections.ArrayList
$list = "apples","pears","oranges","strawberries"
$basket = @{"Basket"=$list;}
$customer = @{"Name"="John";"Surname"="Smith";"OnSubscription"=$true;}

$jsonBase.Add("Data",$basket)
$jsonBase.Add("Customer",$customer)
$jsonBase | ConvertTo-Json -Depth 10 | Out-File ".\basket.json"

This produces the following:

{
    "Customer":  
    {
        "OnSubscription":  true,
        "Name":  "John",
        "Surname":  "Smith"
    },
    "Data":  
    {
        "Basket":
        [
            "apples",
            "pears",
            "oranges",
            "strawberries"
        ]
    }
 }

How about lists containing arrays?

Snippet:

$jsonBase = @{}
$list = New-Object System.Collections.ArrayList

$list.Add(@{"Name"="John";"Surname"="Smith";"OnSubscription"=$true;})
$list.Add(@{"Name"="Daniel";"Surname"="Cray";"OnSubscription"=$false;})
$list.Add(@{"Name"="James";"Surname"="Reed";"OnSubscription"=$true;})
$list.Add(@{"Name"="Jack";"Surname"="York";"OnSubscription"=$false;})

$customers = @{"Customers"=$list;}

$jsonBase.Add("Data",$customers)
$jsonBase | ConvertTo-Json -Depth 10 | Out-File ".\customers.json"

JSON:

{
    "Data": {
        "Customers": [
            {
                "OnSubscription": true,
                "Name": "John",
                "Surname": "Smith"
            },
            {
                "OnSubscription": false,
                "Name": "Daniel",
                "Surname": "Cray"
            },
            {
                "OnSubscription": true,
                "Name": "James",
                "Surname": "Reed"
            },
            {
                "OnSubscription": false,
                "Name": "Jack",
                "Surname": "York"
            }
        ]
    }
}

How about something extra?

Below I have consumed some data from a JSON placeholder api and have separated the data based on whether the ID number is even or odd. I then pipe the results to their own JSON files. To make things easier I use the Invoke-RestMethod command to convert the JSON into a PSCustomObject. This allows me to use this notation $i.id instead of $i["id"]

Here is the snippet:

#Use Invoke-RestMethod to convert to PSCustomObject
$data = Invoke-RestMethod -Uri "https://jsonplaceholder.typicode.com/users"

#Some basic filter logic here
$even = New-Object System.Collections.ArrayList
$odd = New-Object System.Collections.ArrayList
foreach ($i in $data){
    if($i.id % 2 -eq 0) {
        $even.Add($i)
    }
    else {
        $odd.Add($i)
    }

}

$data | ConvertTo-Json -Depth 10 | Out-File ".\api.json"
$even | ConvertTo-Json -Depth 10 | Out-File ".\api-even.json"
$odd | ConvertTo-Json -Depth 10 | Out-File ".\api-odd.json"

To keep things short this is the output for odd numbers:

[
  {
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "[email protected]",
    "address": {
      "street": "Kulas Light",
      "suite": "Apt. 556",
      "city": "Gwenborough",
      "zipcode": "92998-3874",
      "geo": {
        "lat": "-37.3159",
        "lng": "81.1496"
      }
    },
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org",
    "company": {
      "name": "Romaguera-Crona",
      "catchPhrase": "Multi-layered client-server neural-net",
      "bs": "harness real-time e-markets"
    }
  },
  {
    "id": 3,
    "name": "Clementine Bauch",
    "username": "Samantha",
    "email": "[email protected]",
    "address": {
      "street": "Douglas Extension",
      "suite": "Suite 847",
      "city": "McKenziehaven",
      "zipcode": "59590-4157",
      "geo": {
        "lat": "-68.6102",
        "lng": "-47.0653"
      }
    },
    "phone": "1-463-123-4447",
    "website": "ramiro.info",
    "company": {
      "name": "Romaguera-Jacobson",
      "catchPhrase": "Face to face bifurcated interface",
      "bs": "e-enable strategic applications"
    }
  },
  {
    "id": 5,
    "name": "Chelsey Dietrich",
    "username": "Kamren",
    "email": "[email protected]",
    "address": {
      "street": "Skiles Walks",
      "suite": "Suite 351",
      "city": "Roscoeview",
      "zipcode": "33263",
      "geo": {
        "lat": "-31.8129",
        "lng": "62.5342"
      }
    },
    "phone": "(254)954-1289",
    "website": "demarco.info",
    "company": {
      "name": "Keebler LLC",
      "catchPhrase": "User-centric fault-tolerant solution",
      "bs": "revolutionize end-to-end systems"
    }
  },
  {
    "id": 7,
    "name": "Kurtis Weissnat",
    "username": "Elwyn.Skiles",
    "email": "[email protected]",
    "address": {
      "street": "Rex Trail",
      "suite": "Suite 280",
      "city": "Howemouth",
      "zipcode": "58804-1099",
      "geo": {
        "lat": "24.8918",
        "lng": "21.8984"
      }
    },
    "phone": "210.067.6132",
    "website": "elvis.io",
    "company": {
      "name": "Johns Group",
      "catchPhrase": "Configurable multimedia task-force",
      "bs": "generate enterprise e-tailers"
    }
  },
  {
    "id": 9,
    "name": "Glenna Reichert",
    "username": "Delphine",
    "email": "[email protected]",
    "address": {
      "street": "Dayna Park",
      "suite": "Suite 449",
      "city": "Bartholomebury",
      "zipcode": "76495-3109",
      "geo": {
        "lat": "24.6463",
        "lng": "-168.8889"
      }
    },
    "phone": "(775)976-6794 x41206",
    "website": "conrad.com",
    "company": {
      "name": "Yost and Sons",
      "catchPhrase": "Switchable contextually-based project",
      "bs": "aggregate real-time technologies"
    }
  }
]

Thanks for reading!

I hope this helped you in some way, I am currently working on a feature for my IIS Builder script to work at a solution (sln) level rather than at a project (csproj) level. You can read more about it from my post on the Moriyama blog: Automate your local IIS Development Environment.

Some projects can contain multiple web projects in a solution. So far I have seen a solution containing 4 different Umbraco web projects! Having my script 4 times in a project is not ideal so moving the script to work at sln level will hopefully rectify this!

I am currently using these techniques to construct JSON based on any visual studio solution containing a web project. My aim is to have the script read an sln file and identify the web projects in the solution. This JSON can then be used with my IIS Builder script to construct local IIS sites for each web project in the solution. Allowing my colleagues to work more efficiently when facing a new project.