Find and Replace Nested JSON Values with Powershell - json

I have an appsettings.json file that I would like to transform with a PowerShell script in a VSTS release pipeline PowerShell task. (BTW I'm deploying a netstandard 2 Api to IIS). The JSON is structured like the following:
{
"Foo": {
"BaseUrl": "http://foo.url.com",
"UrlKey": "12345"
},
"Bar": {
"BaseUrl": "http://bar.url.com"
},
"Blee": {
"BaseUrl": "http://blee.url.com"
}
}
I want to replace BaseUrl and, if it exists, the UrlKey values in each section which are Foo, Bar and Blee. (Foo:BaseUrl, Foo:UrlKey, Bar:BaseUrl, etc.)
I'm using the following JSON structure to hold the new values:
{
"##{FooUrl}":"$(FooUrl)",
"##{FooUrlKey}":"$(FooUrlKey)",
"##{BarUrl}":"$(BarUrl)",
"##{BleeUrl}":"$(BleeUrl)"
}
So far I have the following script:
# Get file path
$filePath = "C:\mywebsite\appsettings.json"
# Parse JSON object from string
$jsonString = "$(MyReplacementVariablesJson)"
$jsonObject = ConvertFrom-Json $jsonString
# Convert JSON replacement variables object to HashTable
$hashTable = #{}
foreach ($property in $jsonObject.PSObject.Properties) {
$hashTable[$property.Name] = $property.Value
}
# Here's where I need some help
# Perform variable replacements
foreach ($key in $hashTable.Keys) {
$sourceFile = Get-Content $filePath
$sourceFile -replace $key, $hashTable[$key] | Set-Content $filePath
Write-Host 'Replaced key' $key 'with value' $hashTable[$key] 'in' $filePath
}

Why are you defining your replacement values as a JSON string? That's just going to make your life more miserable. If you're defining the values in your script anyway just define them as hashtables right away:
$newUrls = #{
'Foo' = 'http://newfoo.example.com'
'Bar' = 'http://newbaz.example.com'
'Blee' = 'http://newblee.example.com'
}
$newKeys = #{
'Foo' = '67890'
}
Even if you wanted to read them from a file you could make that file a PowerShell script containing those hashtables and dot-source it. Or at least define the values as lists of key=value lines in text files, which can easily be turned into hashtables:
$newUrls = Get-Content 'new_urls.txt' | Out-String | ConvertFrom-StringData
$newKeys = Get-Content 'new_keys.txt' | Out-String | ConvertFrom-StringData
Then iterate over the top-level properties of your input JSON data and replace the nested properties with the new values:
$json = Get-Content $filePath | Out-String | ConvertFrom-Json
foreach ($name in $json.PSObject.Properties) {
$json.$name.BaseUrl = $newUrls[$name]
if ($newKeys.ContainsKey($name)) {
$json.$name.UrlKey = $newKeys[$name]
}
}
$json | ConvertTo-Json | Set-Content $filePath
Note that if your actual JSON data has more than 2 levels of hierarchy you'll need to tell ConvertTo-Json via the parameter -Depth how many levels it's supposed to convert.
Side note: piping the Get-Content output through Out-String is required because ConvertFrom-Json expects JSON input as a single string, and using Out-String makes the code work with all PowerShell versions. If you have PowerShell v3 or newer you can simplify the code a little by replacing Get-Content | Out-String with Get-Content -Raw.

Thank you, Ansgar for your detailed answer, which helped me a great deal. Ultimately, after having no luck iterating over the top-level properties of my input JSON data, I settled on the following code:
$json = (Get-Content -Path $filePath) | ConvertFrom-Json
$json.Foo.BaseUrl = $newUrls["Foo"]
$json.Bar.BaseUrl = $newUrls["Bar"]
$json.Blee.BaseUrl = $newUrls["Blee"]
$json.Foo.Key = $newKeys["Foo"]
$json | ConvertTo-Json | Set-Content $filePath
I hope this can help someone else.

To update values of keys at varying depth in the json/config file, you can pass in the key name using "." between the levels, e.g. AppSettings.Setting.Third to represent:
{
AppSettings = {
Setting = {
Third = "value I want to update"
}
}
}
To set the value for multiple settings, you can do something like this:
$file = "c:\temp\appSettings.json"
# define keys and values in hash table
$settings = #{
"AppSettings.SettingOne" = "1st value"
"AppSettings.SettingTwo" = "2nd value"
"AppSettings.SettingThree" = "3rd value"
"AppSettings.SettingThree.A" = "A under 3rd"
"AppSettings.SettingThree.B" = "B under 3rd"
"AppSettings.SettingThree.B.X" = "Z under B under 3rd"
"AppSettings.SettingThree.B.Y" = "Y under B under 3rd"
}
# read config file
$data = Get-Content $file -Raw | ConvertFrom-Json
# loop through settings
$settings.GetEnumerator() | ForEach-Object {
$key = $_.Key
$value = $_.Value
$command = "`$data.$key = $value"
Write-Verbose $command
# update value of object property
Invoke-Expression -Command $command
}
$data | ConvertTo-Json -Depth 10 | Out-File $file -Encoding "UTF8"

Related

Adding JSON content to PSCustomObject - The property cannot be found on this object

Quite new to objects in PS.
I'm trying to create pscustomobject, adding JSON contents to it via ConvertFrom-JSON and then I'm trying to get contents from another JSON to be set on one of the properties ( nested hierarchy)
$combinedObject=#()
$props = #{
ServiceDefinitions = #()
DataCenters = #()
}
$combinedObject = New-Object -TypeName PSCustomObject -Property $props
$servicedefinitions = Get-ChildItem -Path .\ServiceDefinitions\ | Select Name
$datacenters = Get-ChildItem -Path .\DataCenters\ | Select Name
$environments = #("Production")
$env="TEST"
Foreach ($datacenter in $datacenters)
{
$datacenterdata = $null
write-host "new run"
write-host $datacenter.Name
$datacentername = $datacenter.Name
$datacenterdata = Get-Content -Path .\DataCenters\$datacentername\config.json -Raw
$datacenterformatteddata = $datacenterdata | ConvertFrom-Json -Depth 5
$combinedObject.DataCenters += $datacenterformatteddata
$combinedObject.DataCenters.$datacentername
}
Foreach ($datacenter in $datacenters)
{
$pods = $null
$datacetnername = $null
$datacentername = $datacenter.Name
$pods = Get-ChildItem -Path .\DataCenters\$datacentername\$env\Pod\ | Select Name
Foreach ($pod in $pods)
{
$podname = $pod.Name
$poddata = Get-Content -Path .\DataCenters\$datacentername\$env\Pod\$podname\config.json -Raw
#echo $combinedObject.DataCenters
write-host $datacentername
$podformatteddata = $poddata | ConvertFrom-Json -Depth 5
$combinedObject.DataCenters.$datacentername.pods += $podformatteddata
}
}
For each loop iterations I receive
The property 'pods' cannot be found on this object. Verify that the property exists and can be set.
I can query the pods but cannot set it, it looks to be of a system type System.Object[] do I need to convert it somehow to PSCustomObject for the contents of the next JSON file to be added to it?
Resolved by changing JSON from
pods:[] to
podlist:{ pods:[]}
and referencing
$combinedObject.DataCenters.$datacentername.podlist.pods
to set the value.

Powershell Parse Swagger Json

The following works to parse a Swagger json into resource, method, httptype but probably... the $path.Definition part is weirdly, how can i get $path.Definition to be an array not a string that i need to parse for the array symbol.
$json = Get-Content -Path "$PSScriptRoot/Test/example_swagger.json" | ConvertFrom-Json
$paths = Get-Member -InputObject $json.paths -MemberType NoteProperty
$result = ""
foreach($path in $paths) {
$elements = $path.Name.Substring(5).split("/") -join ","
$httpmethods = $path.Definition.Substring($path.Definition.IndexOf("#{"))
if ($httpmethods.Contains("get")) {
$result += $elements + ", GET" + "`n"
}
if ($httpmethods.Contains("post")) {
$result += $elements + ", POST" + "`n" #same methodnames different http methods
}
}
$result
As detailed in my answer to Iterating through a JSON file PowerShell, the output of ConvertFrom-Json is hard to iterate over. This makes "for each key in object" and "keys of object not known ahead of time" kinds of situations more difficult to handle, but not impossible.
You need a helper function:
# helper to turn PSCustomObject into a list of key/value pairs
function Get-ObjectMember {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True, ValueFromPipeline=$True)]
[PSCustomObject]$obj
)
$obj | Get-Member -MemberType NoteProperty | ForEach-Object {
$key = $_.Name
[PSCustomObject]#{Key = $key; Value = $obj."$key"}
}
}
with this, the approach gets a whole lot simpler:
$swagger = Get-Content -Path "example_swagger.json" -Encoding UTF8 -Raw | ConvertFrom-Json
$swagger.paths | Get-ObjectMember | ForEach-Object {
[pscustomobject]#{
path = $_.Key
methods = $_.Value | Get-ObjectMember | ForEach-Object Key
}
}
Applied to the default Swagger file from https://editor.swagger.io/ as a sample, this is printed
path methods
---- -------
/pet {post, put}
/pet/findByStatus get
/pet/findByTags get
/pet/{petId} {delete, get, post}
/pet/{petId}/uploadImage post
/store/inventory get
/store/order post
/store/order/{orderId} {delete, get}
/user post
/user/createWithArray post
/user/createWithList post
/user/login get
/user/logout get
/user/{username} {delete, get, put}

PowerShell JSON adding value format

I am adding data to a json file. I do this by
$blockcvalue =#"
{
"connectionString":"server=(localdb)\\mssqllocaldb; Integrated Security=true;Database=$database;"
}
"#
$ConfigJson = Get-Content C:\Users\user\Desktop\myJsonFile.json -raw | ConvertFrom-Json
$ConfigJson.data | add-member -Name "database" -value (Convertfrom-Json $blockcvalue) -MemberType NoteProperty
$ConfigJson | ConvertTo-Json| Set-Content C:\Users\user\Desktop\myJsonFile.json
But the format comes out like this:
{
"data": {
"database": {
"connectionString": "server=(localdb)\\mssqllocaldb; Integrated Security=true;Database=mydatabase;"
}
}
}
but I need it like this:
{
"data": {
"database":"server=(localdb)\\mssqllocaldb; Integrated Security=true;Database=mydatabase;"
}
}
}
Can someone help please?
Here's my function to prettify JSON output:
function Format-Json {
<#
.SYNOPSIS
Prettifies JSON output.
.DESCRIPTION
Reformats a JSON string so the output looks better than what ConvertTo-Json outputs.
.PARAMETER Json
Required: [string] The JSON text to prettify.
.PARAMETER Indentation
Optional: The number of spaces to use for indentation. Defaults to 2.
.PARAMETER AsArray
Optional: If set, the output will be in the form of a string array, otherwise a single string is output.
.EXAMPLE
$json | ConvertTo-Json | Format-Json -Indentation 4
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[string]$Json,
[int]$Indentation = 2,
[switch]$AsArray
)
# If the input JSON text has been created with ConvertTo-Json -Compress
# then we first need to reconvert it without compression
if ($Json -notmatch '\r?\n') {
$Json = ($Json | ConvertFrom-Json) | ConvertTo-Json -Depth 100
}
$indent = 0
$Indentation = [Math]::Abs($Indentation)
$regexUnlessQuoted = '(?=([^"]*"[^"]*")*[^"]*$)'
$result = $Json -split '\r?\n' |
ForEach-Object {
# If the line contains a ] or } character,
# we need to decrement the indentation level unless it is inside quotes.
if ($_ -match "[}\]]$regexUnlessQuoted") {
$indent = [Math]::Max($indent - $Indentation, 0)
}
# Replace all colon-space combinations by ": " unless it is inside quotes.
$line = (' ' * $indent) + ($_.TrimStart() -replace ":\s+$regexUnlessQuoted", ': ')
# If the line contains a [ or { character,
# we need to increment the indentation level unless it is inside quotes.
if ($_ -match "[\{\[]$regexUnlessQuoted") {
$indent += $Indentation
}
$line
}
if ($AsArray) { return $result }
return $result -Join [Environment]::NewLine
}
Use it like so:
$ConfigJson | ConvertTo-Json | Format-Json | Set-Content C:\Users\user\Desktop\myJsonFile.json
Replace
(Convertfrom-Json $blockcvalue)
with
(Convertfrom-Json $blockcvalue).connectionString
Then your output object's data.database property will directly contain the "server=(localdb)\\..." value, as desired, not via a nested object that has a connectionString property.
There is one simple Newtonsoft.Json Parser which makes it rly simple to get required format:
Import-Module Newtonsoft.Json
$path = "C:\..."
$json = Get-Content -Path $path -Raw
$parsedJson = [Newtonsoft.Json.Linq.JToken]::Parse($json);
Set-Content $path $parsedJson.ToString();
Enjoy ;)

Serialize JSON from Powershell in a specific fashion

So I have this script that goes out and finds all the software versions installed on machines and lets people know what software and when it was installed across several VMs.
I want to put this on a Dashboard provider we use but they have a specific format in which to use it.
it does produce a valid JSON however I just found out it's not in the format the company wishes.
which would be:
{"table": [["header1", "header2"], ["row1column1", "row1column2"], ["row2column1", "row2column2"]]}
My first thought would be to produce a header row as a beginning variable and then individual variables for each component but that feels very tedious and laborious to create variables for each individual row of data (Date, Name of Software, etc). then at the end combine them into 1 and convert to json
My script is this:
[CmdletBinding()]
Param (
[Parameter(ValueFromPipeline = $true,
ValueFromPipelinebyPropertyName = $true)]
[Alias("Servers")]
[string[]]$Name = (Get-Content "c:\utils\servers.txt")
)
Begin {
}
Process {
$AllComputers = #()
#Gather all computer names before processing
ForEach ($Computer in $Name) {
$AllComputers += $Computer
}
}
End {
ForEach ($Computer in $AllComputers) {
write-output "Checking $computer"
if ($computer -like "*x86*") {
$data = Invoke-Command -cn $computer -ScriptBlock {Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object #{Label = "ServerName"; Expression = {$env:computername}}, DisplayName, Publisher, DisplayVersion, InstallDate | Where-object { $_.Publisher -match "Foobar" }}
$jsondata += $data
}
else {
$data = Invoke-Command -cn $computer -ScriptBlock { Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object #{Label = "ServerName"; Expression = {$env:computername}}, DisplayName, Publisher, DisplayVersion, InstallDate | Where-object { $_.Publisher -match "foobar" } }
$jsondata += $data
}
}
$jsondata | ConvertTo-Json -depth 100 | Out-File "\\servername\C$\Utils\InstalledApps.json"
}
From the sample output format provided I would conclude that you are looking for an array of array. There is a "bug" using ConvertTo-Json when trying to do this but since we need it inside a table object anyway. I will show an example using your code but just on my local computer. Integrating this into your code should not be an issue.
# gather the results
$results = Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-object { $_.Publisher -match "The" } | Select-Object #{Label = "ServerName"; Expression = {$env:computername}}, DisplayName, Publisher, DisplayVersion, InstallDate
# Prepare an array of arrays for the output.
$outputToBeConverted = #()
# build the header
$header = ($results | Get-Member -MemberType NoteProperty).Name
$outputToBeConverted += ,$header
# Add the rows
Foreach($item in $results){
# Create a string array by calling each property individually
$outputToBeConverted += ,[string[]]($header | ForEach-Object{$item."$_"})
}
[pscustomobject]#{table=$outputToBeConverted} | ConvertTo-Json -Depth 5
Basically it is making a jagged array of arrays where the first member is your "header" and each row is built manually from the items in the $results collection.
You will see the unary operator , used above. That is done to prevent PowerShell from unrolling the array. Without that you could end up with one long array in the output.

Access Object From JSON File in Powershell

I have a JSON file that I am reading in Powershell. The structure of the file is below.
[
["computer1", ["program1", versionX]],
["computer2", ["program2", versionY]],
["computer3", ["program3", "versionX"],
["program1", "versionZ"]
],
]
What I want in the program is use $env:computername and compare it with the computerX in the JSON file. If found a match, then iterate through and get the values of programName and ProgramVersion.
However, I don't know how to search through the objects and find ALL items under that.
This is what I have so far.
$rawData = Get-Content -Raw -Path "file.json" | ConvertFrom-Json
$computername=$env:computername
$data = $rawData -match $computername
This gives me objects under it. But how do I iterate through and get individual values?
But don't know what I do after that.
To start you need to be using a valid JSON file
{
"computer1": {
"program1": "versionX"
},
"computer2": {
"program2": "versionY"
},
"computer3": {
"program3": "versionX",
"program1": "versionZ"
}
}
Then you can access the PSObject Properties
$rawData = Get-Content -Raw -Path "file.json" | ConvertFrom-Json
$rawData.PsObject.Properties |
Select-Object -ExpandProperty Name |
ForEach-Object { IF ($_ -eq $env:COMPUTERNAME) {
Write-Host "Computer Name : " $_
Write-Host "Value : " $rawData."$_"
}
}
EDIT for Computer, Program, and Version as separate values
psobject.Properties.Name will give all the program names.
psobject.Properties.Name[0] will give the first program name.
psobject.Properties.value[0] will give the first program version value.
You need to increment the value to get second value, you can also use -1 as a shortcut for the last value.
$rawData = Get-Content -Raw -Path "file.json" | ConvertFrom-Json
$rawData.PsObject.Properties |
Select-Object -ExpandProperty Name |
ForEach-Object { IF ($_ -eq $env:COMPUTERNAME) {
$Computer = $_
$Values = $rawData.$_
}
}
$Computer
$Values.psobject.Properties
$Values.psobject.Properties.Name
$Values.psobject.Properties.Name[0]
$Values.psobject.Properties.value[0]
$Values.psobject.Properties.Name[1]
$Values.psobject.Properties.value[1]
You could also use the program name
$Values.program1
$Values.program2
$Values.program3