Powershell JSON pipeline expand multiple values into one column csv - json

I'm trying to automate some data pipelines with Powershell, but I'm kinda stuck with converting a JSON list to a single cell per row in a CSV file. Hope some of you can help me out.
The JSON I get looks like the following:
{"result": [
{
"uid": "1",
"EducationHistory": []
},
{
"uid": "2",
"EducationHistory": []
},
{
"uid": "3",
"EducationHistory": []
},
{
"uid": "4",
"EducationHistory": {
"10466632": {
"euid": 10466632,
"degree": "Highschool",
"educationLevel": null
},
"10466634": {
"euid": 10466634,
"degree": "Law",
"educationLevel": "batchelor"
},
"10466635": {
"euid": 10466635,
"degree": "Law",
"educationLevel": "master"
}
}
},
{
"uid": "5",
"EducationHistory": {
"10482462": {
"euid": 10482462,
"degree": "IT",
"educationLevel": "master"
}
}
}
]
}
What I want to do is collect the educationLevels per uid in one column. So something like this:
uid | educationLevel
----+------------------
1 |
2 |
3 |
4 | barchelor, master
5 | master
Normally I would like Expandproperty to get down to a lower level, but this doesn't work for this case because every EducationHistory entry is behind a euid for that specific entry. Expanding every single one of them like in the example below isn't workable because of the number of records.
So I think I need something of a loop, but I don't know how. Hope you can help me. First post here and a Powershell newbie, so I hope my question is clear. Please let me know if you need more info.
The code for one entry, as example:
$json = Get-content -raw -path C:\TEMP\File.json
(ConvertFrom-Json -InputObject $json).result |
Select-Object uid,
#Expand one of the entries:
#{Name = "Edu.Level";E={$_.EducationHistory | Select-Object -
expandproperty 10466632 |Select-Object -expandpropert degree }} |
Format-Table

$content = Get-Content .\test.json
$result = ($content | ConvertFrom-Json).result
$totalResult = #()
foreach($res in $result) {
$tempArray = #()
if($res.EducationHistory -ne $null) {
$properties = $res.EducationHistory | Get-Member -MemberType NoteProperty
foreach($property in $properties) {
$eduLevel = $res.EducationHistory.$($property.Name).educationLevel
if(![String]::IsNullOrEmpty($eduLevel)) {
$tempArray += $eduLevel
}
}
}
$totalResult += [PSCustomObject]#{
uid = $res.uid
educationLevel = $tempArray -join ", "
}
}
$totalResult
This will output desired result for the input you have provided.
The trickiest part is the value of EducationHistory property. You have to use Get-Member cmdlet (see Get-Help Get-Member) to get the properties of the current object in loop. Then using the name of the property to access the educationLevel.

Your first question, my first answer I believe :) Similar to the last answer. You need to jump through the hoop of finding the object names in EducationalHistory to reference them.
$json = (Get-content C:\TEMP\File.json | ConvertFrom-Json).result
$results = #()
foreach ( $u in $json)
{
foreach ( $h in $u.EducationHistory)
{
$results += $h.PSObject.properties.Name | ForEach-Object{new-object PSObject -property #{ uid=$u.uid; degree=$h.$_.degree}}
}
}
$results | ConvertTo-Csv | select -skip 1

Related

Extracting fields to CSV from a JSON file with Powershell

i have a json extracted from a API exact like this:
{
"LPEONASVVAP0": {
"LPEONASVVAP0": {
"id": "urn:vcloud:vm:f526d27d-e0f9-4d4f-ae81-4824e397c027",
"name": "LPEONASVVAP0",
"description": "_vm_desc_",
"dateCreated": "2021-04-06T14:56:09.640+0000"
}
},
"WDEONDSVDIS6": {
"WDEONDSVDIS6": {
"id": "urn:vcloud:vm:7ed43492-a7ce-4963-b5bb-5ec2ca89477c",
"name": "WDEONDSVDIS6",
"description": "",
"dateCreated": "2021-04-13T13:44:29.973+0000"
}
},
"WDEONASVSTR0": {
"WDEONASVSTR0": {
"id": "urn:vcloud:vm:7afa34fe-b239-4abe-90df-3f270b44db1f",
"name": "WDEONASVSTR0",
"description": "",
"dateCreated": "2021-03-10T16:17:50.947+0000"
}
},
}
I need extract only fields id, name and description to create a csv with them. I test this but the output file is in blank:
$pathToJsonFile = x
$pathToOutputFile = x
$obj = Get-Content $pathToJsonFile -Raw | ConvertFrom-Json
print $obj
$obj | select id, name, description | Convertto-csv > $pathToOutputFile
You'll need to "discover" the parent property names (eg. 'LPEONASVVAP0') via the psobject hidden memberset. Since the outer and inner properties are named the same, we can re-use the name to get the inner property value:
$obj.psobject.Properties |ForEach-Object {
$_.Value.$($_.Name)
} |Select id,name,description |Export-Csv -NoTypeInformation -Path $pathToOutputFile
Edit: Mathias R. Jessens answer is better written than this, i would do it that way instead of how i posted.
Okay so i copied the json you posted, imported it. Since each array of information is stored like this
"WDEONDSVDIS6": {
"WDEONDSVDIS6": {
i used get-member to iterate each of the arrays and then select the info from that.
Also, you dont need to use Convertto-csv > $pathToOutputFile, use the export-csv command instead.
Below is my code how i would have done it, there is probably a better way but this works :)
$pathToOutputFile = x.csv
$obj = Get-Content example.json -Raw| ConvertFrom-Json
$obj2 = ($obj | Get-Member -MemberType noteproperty).Name
$result = foreach($item in $obj2){
$obj.$item.$item | select id,name,description
}
$result | Export-Csv -Path $pathToOutputFile -Encoding utf8 -NoTypeInformation

PowerShell Convert CSV to nested Json

I try to create a script that will import data given in a csv file to an API, in order to do this, each line of the CSV has to be converted to Json and then posted to the API.
My base data in the csv looks like this:
Users.Name,Users.Mail,Users.AdditionalInfo.String1,Users.AdditionalInfo.Int1
System.String,System.String,System.String,System.Int32
MyName,my#name.com,Hello,1
YourName,your#name.com,GoodBye,2
The first line contains the Json Information, where "Users" is the API resource the data is written to. "Users" can be any other object accessible via the API also, see my comment below.
The second line is to identify the type of the value. This is for later use and not used right now, beginning with line three is data.
The "." is used to specify the "level" of nesting, so the first "." separates the API Object from the first level in the json, the second "." the first level from the second level.
I want a Json to be returned like this:
{
"Name": "MyName",
"Mail": "my#mail.com",
"AdditionalInfo": {
"String1": "Hello",
"Int1": 1
}
}
I use the following code:
$objects = Import-Csv "test.csv" -Encoding UTF8
$validObjects = $objects | Select-Object -Skip 1
$validObjects
ForEach ($object in $validObjects) {
#$apiObject = ($object.psobject.properties.name).Split(".")[0]
$jsonObject = #{}
ForEach ($property in $object.psobject.properties) {
switch($property.Name.Split(".").Count - 1) {
1 {
$jsonObject.Add($property.Name.Split(".")[1], $property.Value)
}
2 {
$tempJsonObject = #{$property.Name.Split(".")[2] = $property.Value}
$jsonObject.Add($property.Name.Split(".")[1], $tempJsonObject)
}
}
}
$jsonObject | ConvertTo-Json
##($object.psobject.properties).Count
}
The problem i face now is that since i have two colums starting with "Users.AdditionalInfo" ist will run into an error, because you can add "AdditionalInfo" only as a key once. Is there an easy way around it?
Seems pretty crazy to have it set up this way. Surely XML would be a better format for the needs. That said, here is what I came up with.
Setting up sample file so others can try this themselves.
$tempfile = New-TemporaryFile
#'
Users.Name,Users.Mail,Users.AdditionalInfo.String1,Users.AdditionalInfo.Int1
System.String,System.String,System.String,System.Int32
MyName,my#name.com,Hello,1
YourName,your#name.com,GoodBye,2
'# | Set-Content $tempfile -Encoding utf8
$csvdata = Import-Csv $tempfile | select -skip 1
Now here's my crazy script
$csvdata | foreach {
$ht = [ordered]#{}
,#($_.psobject.properties) | foreach {
$subprops,$props = $_.where({$_.name -match 'additionalinfo'},'split')
$props | foreach {$ht.Add($_.name,$_.value)}
,#($subprops) | foreach {
$ht.Add("AdditionalInfo",(#{
($_[0].name.split(".")[2]) = $_[0].value
($_[1].name.split(".")[2]) = $_[1].value
}))
}
}
$ht
} | ConvertTo-Json -OutVariable jsonresults
And the output shown plus stored in $jsonresults
[
{
"Users.Name": "MyName",
"Users.Mail": "my#name.com",
"AdditionalInfo": {
"String1": "Hello",
"Int1": "1"
}
},
{
"Users.Name": "YourName",
"Users.Mail": "your#name.com",
"AdditionalInfo": {
"String1": "GoodBye",
"Int1": "2"
}
}
]
A trick I'm sure I picked up from mklement or Mathias that was used twice is
,#(some objects that normally get passed one by one) | foreach
When you add the comma and the array construct, it passes all elements as one vs one at at time. Very helpful in certain times, like these for sure.
The other trick I wanted to highlight is this line
$subprops,$props = $_.where({$_.name -match 'additionalinfo'},'split')
The where method has several modes, one being split. Any that matched additional went to the first variable, the rest went to the second. Hopefully this helps you complete your project.
Edit
Since the items like additionalinfo can be different with varying amount of subproperties, here is a version that will accommodate.
$csvdata | foreach {
$ht = [ordered]#{}
,#($_.psobject.properties) | foreach {
$subprops,$props = $_.where({($_.name.split("."))[2]},'split')
$props | foreach {$ht.Add($_.name,$_.value)}
$subs = $subprops | foreach {$_.name.split(".")[1]} | Select -Unique
foreach($name in $subs)
{
,#($subprops | where name -match $name) | foreach {
$oht = [ordered]#{}
$_ | foreach {$oht[$_.name.split(".")[2]] = $_.value}
$ht.Add($name,$oht)
}
}
}
$ht
} | ConvertTo-Json -OutVariable jsonresults -Depth 5
First data set output
[
{
"Users.Name": "MyName",
"Users.Mail": "my#name.com",
"AdditionalInfo": {
"String1": "Hello",
"Int1": "1"
}
},
{
"Users.Name": "YourName",
"Users.Mail": "your#name.com",
"AdditionalInfo": {
"String1": "GoodBye",
"Int1": "2"
}
}
]
#'
Users.Name,Users.Mail,Users.AnotherPossibility.String1,Users.AnotherPossibility.Int1,Users.AnotherPossibility.Int2,Users.AdditionalInfo.String1,Users.AdditionalInfo.Int1
System.String,System.String,System.String,System.Int32,System.String,System.Int32
MyName,my#name.com,Hello,1,3,Bonjour,5
YourName,your#name.com,GoodBye,2,4,Adios,6
'# | Set-Content $tempfile -Encoding utf8
Second data set output
[
{
"Users.Name": "MyName",
"Users.Mail": "my#name.com",
"AnotherPossibility": {
"String1": "Hello",
"Int1": "1",
"Int2": "3"
},
"AdditionalInfo": {
"String1": "Bonjour",
"Int1": "5"
}
},
{
"Users.Name": "YourName",
"Users.Mail": "your#name.com",
"AnotherPossibility": {
"String1": "GoodBye",
"Int1": "2",
"Int2": "4"
},
"AdditionalInfo": {
"String1": "Adios",
"Int1": "6"
}
}
]

Reading a json file in key value pair in the same order that's given in input

I am writing a PowerShell Script, which will read a json file having different sections, like job1, job2 and so on.. Now my objective is to read each section separately and to loop through it as a key value pair. I also need to maintain the order of the input file, because the jobs are scheduled in sequence. and these jobs run taking the values from the json file as input.
I tried using Powershell version 5.1, in which I created PSCustomObject but the order is getting sorted alphabetically, which I DON'T want.
Json File :
{ "Job1": [
{
"Ram" : "India",
"Anthony" : "London",
"Elena" : "Zurich"
}],
"Job2": [
{
"Build" : "fail",
"Anthony" : "right",
"Sam" : "left"
}]}
$json = Get-Content -Path C:\PowershellScripts\config_File.json |
ConvertFrom-Json
$obj = $json.Job1
$json.Job1 | Get-Member -MemberType NoteProperty | ForEach-Object {
$key = $_.Name
$values = [PSCustomObject][ordered]#{Key = $key; Value = $obj."$key"}
$values
}
I am expecting to loop through each section separately and in the same order that's provided in the json file. For example looping through Job1 section and to fetch only the Values in the same order that's in the json file.
I will guarantee that this is not the best way to do this, but it works.
$json = Get-Content -Path C:\PowershellScripts\config_File.json |
ConvertFrom-Json
$out = ($json.Job1 | Format-List | Out-String).Trim() -replace "\s+(?=:)|(?<=:)\s+"
$out -split "\r?\n" | ForEach-Object {
[PSCustomObject]#{Key = $_.Split(":")[0]; Value = $_.Split(":")[1]}
}
Explanation:
The JSON object is first output using Format-List to produce the Property : Value format, which is piped to Out-String to make that output a single string. Trim() is used to remove surrounding white space.
The -replace removes all white space before and after : characters.
The -split \r?\n splits the single string into an array of lines. Each of those lines is then split by the : character (.Split(":")). The [0] index selects the string on the left side of the :. The [1] selects the string on the right side of the :.
Can you change the json schema?
I would probably make changes to the json schema before i tried to parse this (if possible of course).
Like this (changed only Job1):
$json = #"
{ "Job1": [
{
"Name": "Ram",
"Location" : "India"
},
{
"Name": "Anthony",
"Location": "London"
},
{
"Name": "Elena" ,
"Location": "Zurich"
}
],
"Job2": [
{
"Build" : "fail",
"Anthony" : "right",
"Sam" : "left"
}]}
"# | convertfrom-json
foreach ($obj in $json.Job1) {
$key = $obj.Name
$values = [PSCustomObject][ordered]#{Key = $key; Value = $obj."$key" }
$values
}

Nested json extract from powershell

I need to show id_products as a result, the remaining data I have a photo id and url pictures.
Below is the code which I extract data from
$files_ro = "products.csv"
$Ident = Import-Csv -Path $files_ro -Header id_product | select-object -skip 1
foreach ($idka in $ident)
{
$idp = $idka.id_product
$request_n = "http://api.url/"+ $idp +""
foreach($d1 in $request_n)
{
Invoke-WebRequest $d1 |
ConvertFrom-Json |
Select-Object -Expand data |
Select -expand extended_info |
select -expand images |
Select id,url
}
}
files
- product.csv
"id_product"
"21221"
"23526"
"23525"
"24074"
"21302"
"24372"
"21272"
"21783"
"27268"
"21776"
json
{
data: {
id: 21221,
extended_info: {
images: [
{
id: 34380,
url: photos1.jpg
},
{
id: 34381,
url: photos2.jpg
},
{
id: 34382,
url: photos3.jpg
}
],
}
}
}
I would like it to look like this:
id_product,id(images), url
21221,34380,photos1.jpg
21221,34381,photos2.jpg
21221,34382,photos3.jpg
You can help me somehow ?
Your provided JSON is not valid. However, I would use a PSCustomObject to create the desired result:
$json = #'
{
"data": {
"id": 21221,
"extended_info": {
"images": [{
"id": 34380,
"url": "photos1.jpg"
}, {
"id": 34381,
"url": "photos2.jpg"
}, {
"id": 34382,
"url": "photos3.jpg"
}
]
}
}
}
'# | ConvertFrom-Json
$json.data.extended_info.images | ForEach-Object {
[PSCustomObject]#{
id_product = $json.data.id
"id(images)" = $_.id
url = $_.url
}
}
Output:
id_product id(images) url
---------- ---------- ---
21221 34380 photos1.jpg
21221 34381 photos2.jpg
21221 34382 photos3.jpg
To convert the result to CSV, just add | ConvertTo-Csv -NoTypeInformation after the last curly bracket to get the following output:
"id_product","id(images)","url"
"21221","34380","photos1.jpg"
"21221","34381","photos2.jpg"
"21221","34382","photos3.jpg"
As your product.csv already has the same header it isn't necessary to supply one and then skip the first line.
Your single URL $request_n also doesn't need to be iterated with a foreach
I suggest to store the result from the webrequest and convertedfrom-json into var $Json and proceed with Martin Brandls good answer.
## Q:\Test\2019\05\24\SO_56287843.ps1
$files_ro = "products.csv"
$Ident = Import-Csv -Path $files_ro
$Data = foreach ($idka in $ident){
$request_n = "http://api.url/{0}" -f $idka.id_product
$Json = Invoke-WebRequest $request_n | ConvertFrom-Json
# inserted code from Martin Brandl's good answer
$Json.data.extended_info.images | ForEach-Object {
[PSCustomObject]#{
id_product = $json.data.id
"id(images)" = $_.id
url = $_.url
}
}
}
$Data
$Data | Export-Csv ProductImages.csv -NoTypeInformation
#$Data | Out-Gridview

Nested arrays and ConvertTo-Json

To use a REST API, I must pass a JSON object that looks like this:
{ "series" :
[{
"metric": "custom.powershell.gauge",
"points":[[1434684739, 1000]]
}
]
}
Note the nested array here. I cannot get to reproduce this. Here is my code:
[int][double]$unixtime=get-date ( (get-date).ToUniversalTime() ) -UFormat %s
$obj=#{}
$series=#{}
$array=#()
$points=#()
$value=get-random -Minimum 0 -Maximum 100
$series.add("metric","custom.powershell.gauge")
$points=#(#($unixtime, $value))
$series.add("points",$points)
$obj.Add("series",#($series))
$json=$obj | ConvertTo-Json -Depth 30 -Compress
$json
And here is the output:
{"series":[{"points":[1434685292,95],"metric":"custom.powershell.gauge"}]}
I've tried many things, I cannot get the 2 arrays to be nested, it always end up looking like a single array.
On the same note, came someone explain this please:
> $a=(1,2)
> $a
1
2
> $a | ConvertTo-Json
[
1,
2
]
> $b=($a,$a)
> $b
1
2
1
2
> $b | ConvertTo-Json
[
{
"value": [
1,
2
],
"Count": 2
},
{
"value": [
1,
2
],
"Count": 2
}
]
Where are these value and Count coming from?
Thanks for your help.
The explanation is that (1,2),(3,4) is an array of array, but Powershell split the first level with the pipe |, and you don't give a name for these arrays so the serializer supplies it. First have a try to this :
# First build your array of array
$z = (1,2),(3,4)
# convert it to JSON using the ,
,$z | ConvertTo-Json -Depth 5 -Compress
[psobject]#{"points"=$z} | ConvertTo-Json -Depth 5 -Compress
It gives the first step:
{"value":[[1,2],[3,4]],"Count":2}
{"points":[[1,2],[3,4]]}
Now the solution I propose :
# First build your array of array
$z = (1,2),(3,4)
# Then build a PSCustom object
$a = [pscustomobject]#{"series" = ,#{"metric"="custom.powershell.gauge"; "points"=$z}}
# At the end convert it to JSON
# don't forget the **Depth** parameter (use **Compress** to retreive one line like above)
$a | ConvertTo-Json -Depth 5
For me it gives something close to what you need:
{
"series": [
{
"points": [
[
1,
2
],
[
3,
4
]
],
"metric": "custom.powershell.gauge"
}
]
}
Late to the party, but I'd like to propose a more visually intuitive solution that's easy to expand (I, like others, am a visual learner so code blocks like the below help me understand things more easily):
[int][double]$unixtime = Get-Date ((Get-Date).ToUniversalTime()) -UFormat %s
$value = Get-Random -Minimum 0 -Maximum 100
$body = #{
'series' = #(
[Ordered]#{
'metric'='custom.powershell.gauge'
'points' = #(
,#($unixtime,$value)
)
}
)
}
ConvertTo-Json -InputObject $body -Depth 4
Outputs:
{
"series": [
{
"metric": "custom.powershell.gauge",
"points": [
[
1473698742,
96
]
]
}
]
}
-Depth 4 gets you the additional set of square brackets around your point values, and [Ordered] ensures that the hashtable is ordered as originally specified. Don't forget -Compress before sending, like others have said.
As JPBlanc suggested, creating a custom object worked. Below is my code:
[long]$value=Get-Random -Minimum 0 -Maximum 100
$points=,#($unixtime, $value)
$metricname="custom.powershell.gauge"
$obj = [pscustomobject]#{"series" = ,#{"metric" = $metricname; "points"=$points}}
$json=$obj | ConvertTo-Json -Depth 5 -Compress
Which outputs:
{"series":[{"points":[[1434810163,53]],"metric":"custom.powershell.gauge"}]}
Don't forget to specify a depth >2.
Thanks!