2

I use powershell to loop through multiple Json files obtained by the REST API. But the difficulty is that there are nodes with \ in them, and the name of a node is not fixed, we have no way to get it, wildcards seem to have no effect.

My test Json file Workflow_Info like:

{
   "id": "/subscriptions/fcxxxx7/resourceGroups/xxxxxx/providers/Microsoft.Web/sites/xxxx/workflows/Test_email",
   "name": "xxxxxxxxx/Test_email",
   "type": "Microsoft.Web/sites/workflows",
   "kind": "Stateful",
   "location": "East Asia",
   "properties": {
      "files": {
         "Test_email/workflow.json": {
            "definition": {
               "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
               "actions": {
                  "Send_an_email_from_a_shared_mailbox_(V2)": {
                     "inputs": {
                        "host": {
                           "connection": {
                              "referenceName": "TheValueIwant"
                           }
                        },
                        "method": "post",
                        "path": "/v2/SharedMailbox/Mail"
                     },
                     "runAfter": {},
                     "type": "ApiConnection"
                  }
               },
               "contentVersion": "1.0.0.0",
               "outputs": {}
            },
            "kind": "Stateful"
         }
      }
   }
}

What I want to get is the value of the node referenceName.

But there are two difficulties:

  1. The node Test_email\workflow.json includes the \ in it, how to parse it?

  2. The node Send_an_email_from_a_shared_mailbox_(V2) it is not a fixed value, it has different value in different json, I tried to use wildcard *, but not work.

My test powershell scripts is:

   $WorkflowJson = $Workflow_Info | ConvertTo-Json -depth 100
   $WorkflowJsonNew = $Workflow_Info.properties.files."$($WorkflowName)/workflow.json".definition.'*'.host.connection.referenceName | ConvertTo-Json -depth 100
   Write-Host "The workflow is:$WorkflowJsonNew"
Joy
  • 1,171
  • 9
  • 15
  • 2
    I don’t think your json is valid - ```\``` starts an escape sequence in a json string - e.g. ```\r``` for a CR, ```\t``` for a tab, etc, so it’s trying to parse the ```\w``` in ```Test_email\workflow.json``` as an escape sequence, but ```w``` *isn’t* a valid escape character so you get a parse error. If you want a literal ```\``` in a json string you need to escape it as ```\\``` - you need to ask whoever is generating your json to fix it, or do some truly horrible regex replacement to try to fix it yourself in the raw json text before you parse it. – mclayton Apr 21 '23 at 14:18
  • @mclayton, I modified it, but I'm not super sure that it works. Because the original Json file is very large and contains a lot of confidential information, I am sorry that I cannot share the complete Json file. But this one shouldn't affect my question since it's clear. Can you give me some specific advice? Or you can give me a Json sample to demonstrate your solution and I'll look into it. Thanks – Joy Apr 24 '23 at 02:47
  • @mclayton, For the coomment "If you want a literal \ in a json string you need to escape it as \\", that means I need to change the \ to \\ in the Json file? – Joy Apr 24 '23 at 02:51
  • 2
    @Joy That would be a dirty workaround and you may encounter different escaping issues in the future. The real solution would be to go to the source and tell it to deliver valid JSON. The "JSON" you posted is just invalid. It may look like JSON, but it is not JSON. So there is no point to try to parse it as JSON. – stackprotector Apr 24 '23 at 06:49
  • 2
    @joy - try this and you’ll see what a valid file should look like with ```\``` in the data: ```@{ "files" = @{ "Test_email\workflow.json" = @{ "aaa" = "bbb" } } } | ConvertTo-Json```. Note that in general you can’t just replace ```\``` with ```\\``` in the json file because that will break valid sequences like ```\r```, but maybe that’s not an issue in your specific data. A better fix would be to properly encode the data in the first place when generating the json file. – mclayton Apr 24 '23 at 06:59
  • @mclayton, I have update the Json, change "Test_email\workflow.json" to Test_email/workflow.json, and I check it with JSON Formatter, it should be correct. – Joy Apr 24 '23 at 07:31

4 Answers4

2

After your edits to the question that changed \ to /, your first question no longer applies, and your own answer is no longer needed.

  • The only thing worth noting with respect to your first question is that you need quoting in order to access properties whose names contain unusual characters such as \ and /, as already shown in the code in your question:
    • $Workflow_Info.properties.files."$($WorkflowName)/workflow.json"

      • Note that it can be simplified to $Workflow_Info.properties.files."$WorkflowName/workflow.json", given that you don't strictly need $(...) inside expandable (double-quoted) string ("...") for stand-alone variable references such as $WorkflowName - however, you do need $(...) for expressions, such as $Workflows[0] or $Workflow.Name
    • For verbatim names with unusual names, use verbatim (single-quoted) strings ('...'), e.g.. ([pscustomobject] @{ 'foo\bar' = 42 }).'foo\bar'


As for your second question:

A simplified example:

# Get all .Parent property values, if any, from the values of all
# properties that the System.IO.FileInfo instance returned by Get-ChildItem has.
(Get-ChildItem $PROFILE).psobject.Properties.Value.Parent
  • Note the psobject.Properties.Value part, which enumerates all property values and looks for a .Parent property on each.

    • That is, it is the equivalent of combining the results of (Get-ChildItem $PROFILE).Name, (Get-ChildItem $PROFILE).Length, ..., over all properties that a FileInfo instance has.
  • Situationally, there may be NO results ($null), ONE result, or an ARRAY of results.

    • In the case at hand, only the DirectoryInfo instances stored in the .Directory property has a .Parent property, so that a single result - another DirectoryInfo instance representing the parent directory of the directory in which the $PROFILE file is located - is returned.

    • By contrast, if you used .Name, you'd get three results, because the objects stored in the .PSDrive, .PSProvider, and .Directory properties all have a .Name property.

  • Note that it is possible to use multiple .psobject.Properties.Value expressions in a single property path, one for each level of the object-graph hierarchy where the property name(s) are unknown.

    • However, you always need to know at what level of the hierarchy the target value is to be found and provide the appropriate number of path components.

    • If not known, you need a solution that walks an entire object graph (hierarchy of nested objects) to look for properties of interest, as shown in iRon's answer.

Applied to your case (this is a self-contained example that can be run as-is):

$fromJson = ConvertFrom-Json @'
{"id":"/subscriptions/fcxxxx7/resourceGroups/xxxxxx/providers/Microsoft.Web/sites/xxxx/workflows/Test_email","name":"xxxxxxxxx/Test_email","type":"Microsoft.Web/sites/workflows","kind":"Stateful","location":"East Asia","properties":{"files":{"Test_email/workflow.json":{"definition":{"$schema":"https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#","actions":{"Send_an_email_from_a_shared_mailbox_(V2)":{"inputs":{"host":{"connection":{"referenceName":"TheValueIwant"}},"method":"post","path":"/v2/SharedMailbox/Mail"},"runAfter":{},"type":"ApiConnection"}},"contentVersion":"1.0.0.0","outputs":{}},"kind":"Stateful"}}}}
'@

$WorkflowName = 'Test_email'

# Again, note the `psobject.Properties.Value` part.
# -> 'TheValueIwant'
$fromJson.properties.files."$WorkflowName/workflow.json".definition.actions.
  psobject.Properties.Value.inputs.host.connection.referenceName
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Thank you very much for your input, your detailed explanation helped me a lot, iRon's function will be more applicable to me. But still thank you for your reply – Joy Apr 27 '23 at 05:47
1

The second question is a duplicate of:
(But as it has a bounty, it can be closed as such)

And can best resolved with the following function:

 function Get-Node {
  [CmdletBinding()][OutputType([Object[]])] param(
    [ScriptBlock]$Where,
    [AllowNull()][Parameter(ValueFromPipeLine = $True, Mandatory = $True)]$InputObject,
    [Int]$Depth = 10
  )
  process {
    if ($_ -isnot [String] -and $Depth -gt 0) {
      if ($_ -is [Collections.IDictionary]) {
        if (& $Where) { $_ }
        $_.get_Values() | Get-Node -Where $Where -Depth ($Depth - 1)
      }
      elseif ($_ -is [Collections.IEnumerable]) {
        for ($i = 0; $i -lt $_.get_Count(); $i++) { $_[$i] | Get-Node -Where $Where -Depth ($Depth - 1) }
      }
      elseif ($Nodes = $_.PSObject.Properties.Where{ $_.MemberType -eq 'NoteProperty' }) {
        $Nodes.ForEach{
          if (& $Where) { $_ }
          $_.Value | Get-Node -Where $Where -Depth ($Depth - 1)
        }
      }
    }
  }
}

Usage

Finding node(s) with a specific name and value (-format):

$Node = $Workflow_Info.properties.files.'Test_email/workflow.json'.definition.actions |Get-Node -Where { $_.Name -eq 'referenceName' }
$Node.Value
TheValueIwant

Replacing the value of the found node(s):

$Node |ForEach-Object {
    $_.Value  = 'My new value for: ' + $_.Value
}
$Workflow_Info |ConvertTo-Json -Depth 15

Results

{
  "id": "/subscriptions/fcxxxx7/resourceGroups/xxxxxx/providers/Microsoft.Web/sites/xxxx/workflows/Test_email",
  "name": "xxxxxxxxx/Test_email",
  "type": "Microsoft.Web/sites/workflows",
  "kind": "Stateful",
  "location": "East Asia",
  "properties": {
"files": {
  "Test_email/workflow.json": {
    "definition": {
      "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
      "actions": {
        "Send_an_email_from_a_shared_mailbox_(V2)": {
          "inputs": {
            "host": {
              "connection": {
                "referenceName": "My new value for: TheValueIwant"
              }
            },
            "method": "post",
            "path": "/v2/SharedMailbox/Mail"
          },
          "runAfter": {},
          "type": "ApiConnection"
        }
      },
      "contentVersion": "1.0.0.0",
      "outputs": {}
    },
    "kind": "Stateful"
  }
}
  }
}
iRon
  • 20,463
  • 10
  • 53
  • 79
  • 1
    Been very busy lately, haven't had time to test your solution until now, it works like magic. really thankful – Joy Apr 27 '23 at 05:44
0

First: The JSON isn't valid on multiple places.

=> Known problem with "\"

$Workflow_Info = (Get-Content -Path "C:\userdata\Stackoverflow\out.json" -Raw) -replace "\\", "\\"

This is a litle bit dirty. But it works in your case, because there are no further backslashes. At least, a clean solution has been provided from the developer of the rest service. Currently the JSON is out of standard, see https://datatracker.ietf.org/doc/html/rfc8259 => Chapter "7. Strings"

=> Row 28 => "outputs": {}, => replace the "," at the end, because the list object has been finished.
=> Row 32 => }, => replace the "," at the end, because the list object has been finished.

To ensure a valid JSON, you should take a look here: https://jsonlint.com/
I'm using this since years.

Second: Your question about how to capture a unknown property name.

# Getting the raw JSON file:
$Local:Workflow_Info_AsJsonString = (Get-Content -Path "C:\userdata\Stackoverflow\out.json" -Raw) -replace "\\", "\\"
# Converting the JsonString to a PSObject
$Local:Workflow_Info_AsPSObject   = ConvertFrom-Json -InputObject $Workflow_Info_AsJsonString
# Getting the name of the property with the changing name.  (Preconditioned there is only one object under "actions".
$Local:PropertyWithChangingName    = ($Workflow_Info_AsPSObject.properties.files.'Test_email\workflow.json'.definition.actions).PSObject.Properties.Name
# Accessing a property behind the property with the changing name
$Workflow_Info_AsPSObject.properties.files.'Test_email\workflow.json'.definition.actions.($PropertyWithChangingName).inputs.path  

Please excuse my bad English. Computer languages are easier to learn as human languages. Regards from Bavaria.

Buxmaniak
  • 460
  • 2
  • 4
  • 1
    Per @mklement’s comment on your other answer at https://stackoverflow.com/a/76074503/3156906, using ```$Local:``` is a bit jarring to me personally, and would likely be a distraction to a new PowerShell user reading your answer. I’m not going to downvote because of it, but imho I think your answer would be improved by removing the ```$local:```… – mclayton Apr 21 '23 at 18:44
  • I modified it, but I'm not super sure that it works. Because the original Json file is very large and contains a lot of confidential information, I am sorry that I cannot share the complete Json file. But this one shouldn't affect my question since it's clear. Can you give me some specific advice? Or you can give me a Json sample to demonstrate your solution and I'll look into it. Thanks – Joy Apr 24 '23 at 02:49
0

For the first question, I have resolved it by add "" for the Test_email/workflow.json to get it:

$WorkflowJson = $Workflow_Info | ConvertTo-Json -depth 100
$WorkflowJsonNew = $WorkflowJson .properties.files."$($WorkflowName)/workflow.json" | ConvertTo-Json -depth 100

Now I could get the value.

But for the second question, I will open another question for some more details.

Joy
  • 1,171
  • 9
  • 15