Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add -AsHashtable switch to ConvertFrom-Json for issue #3623 #5043

Merged
merged 13 commits into from
Oct 25, 2017

Conversation

bergmeister
Copy link
Contributor

@bergmeister bergmeister commented Oct 6, 2017

Address #3623.

Implementation symmetric to traditional code path that returns a PSCustomObject. Code is shared on the top level but 2 low level methods were difficult to share, therefore 2 separate but similar methods were created for HashTables.
All existing tests related to this change were adapted to also test against this new switch and were slightly improved.

Update (@TravisEz13):
-AsHashtable is an existing pattern used in Group-Object

Copy link
Collaborator

@iSazonov iSazonov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bergmeister Thanks for great contribution!

[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly")]
public static object ConvertFromJson(string input, out ErrorRecord error)
public static object ConvertFromJson(string input, bool returnHashTable, out ErrorRecord error)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't change a public API. Please add new overload.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I did not know PowerShell also exposes some of its classes. I'll re-add the old signature API that will then internally call the new overload.

@{ name = "object"; str = "{a:1}"; ReturnHashTable = $true }
@{ name = "empty"; str = ""; ReturnHashTable = $false }
@{ name = "spaces"; str = " "; ReturnHashTable = $false }
@{ name = "object"; str = "{a:1}"; ReturnHashTable = $false }
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move to BeforeAll block.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what you exactly mean by that. I removed the redundant line about `$TestCasesForReturnHashTableParameter that was a leftover.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see sample
We should place $TestCasesForReturnHashTableParameter and $validStrings in the BeforeAll block.

)

It 'no error for valid string - <name>' -TestCase $validStrings {
param ($str)
It 'no error for valid string - <name> when ReturnHashTable is <ReturnHashTable>' -TestCase $validStrings {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe 'no error for valid string - with -ReturnHashTable:`$<ReturnHashTable>'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed to match your suggestion whilst still displaying the name of the string.

/// <summary>
/// Returned data structure is a Hashtable instead a CustomPSObject.
/// </summary>
[Parameter(Mandatory = false)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is default. Please remove.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by that? Can you elaborate please (what is 'it')?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Parameter(Mandatory = false)] is default

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I see, thanks.

else
{
obj = PopulateFromJArray(list, out error);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add 'else' with Diagnostics.Assert (if the object is not JObject or JArray).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of the cast to JArray a few lines above, obj cannot be something else. I initially put your suggestion in but even the compiler tells me that:
error CS0184: The given expression is never of the provided ('JObject') type

Copy link
Collaborator

@iSazonov iSazonov Oct 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant:

                        Diagnostics.Assert(
                            false,
                            "Only JObject or JArray is supported.");

…tch since the parameter is not mandatory by default
…ld have been only the Mandatory=false part that should be removed.
@@ -171,7 +171,7 @@ private bool TryConvertToJson(string json, out object obj, ref Exception exRef)
try
{
ErrorRecord error;
obj = JsonObject.ConvertFromJson(json, out error);
obj = JsonObject.ConvertFromJson(json, false, out error);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert the change - we have the overload.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right.

@{ name = "plain text"; str = "plaintext"; ReturnHashTable = $true }
@{ name = "part"; str = '{"a" :'; ReturnHashTable = $true }
@{ name = "plain text"; str = "plaintext"; ReturnHashTable = $false }
@{ name = "part"; str = '{"a" :'; ReturnHashTable = $false }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we have a check for input == null but haven't the test - please add new test with str = $null.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Added

…that is not necessary any more since the original method signature has been restored.

-Improve test to add case for $null
@TravisEz13 TravisEz13 added WG-Cmdlets-Utility cmdlets in the Microsoft.PowerShell.Utility module Review - Committee The PR/Issue needs a review from the PowerShell Committee and removed Review - Committee The PR/Issue needs a review from the PowerShell Committee labels Oct 10, 2017
@TravisEz13
Copy link
Member

TravisEz13 commented Oct 10, 2017

-AsHashtable is an existing pattern used in Group-Object. I don't think this needs committee review.

@iSazonov
Copy link
Collaborator

We have discussion for this #3623

@TravisEz13
Copy link
Member

@dantraMSFT Can you review this?

@SteveL-MSFT
Copy link
Member

As part of this PR, if we can detect that the json has an property named empty string for the pscustomobject path and provide an error message like:

The provided json includes a property who name is an empty string, this is only supported using the `-AsHashTable` switch.

Then we can resolve #1755 as fixed.

… -AsHashTable via error message. Fixed a typo that I found in the message strings.
<value>Path '{0}' resolves to a directory. Specify a path including a file name, and then retry the command.</value>
</data>
<data name="EmptyKeyInJsonString" xml:space="preserve">
<value>The provided json includes a property whose name is an empty string, this is only supported using the -AsHashTable switch.</value>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably capitalize json as JSON

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Fixed it.

Copy link
Collaborator

@iSazonov iSazonov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we address the comment about case sensitivity?

)

It 'no error for valid string - <name>' -TestCase $validStrings {
param ($str)
It 'no error for valid string ''<name>'' with -ReturnHashTable:$<ReturnHashTable>' -TestCase $validStrings {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please start test descriptions with capital.

We use $validStrings one time. Please use our common template:

It 'no error for valid string ''<name>'' with -ReturnHashTable:$<ReturnHashTable>' -TestCase @(
 @{ name = "null";   str = $null;   ReturnHashTable = $true  }
 ...
) {
...
}

)

It 'throw ArgumentException for invalid string - <name>' -TestCase $invalidStrings {
param ($str)
It 'throw ArgumentException for invalid string ''<name>'' with -ReturnHashTable:$<ReturnHashTable>' -TestCase $invalidStrings {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same.

{ [Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson($str, $ReturnHashTable, [ref]$errRecord) } | ShouldBeErrorId "ArgumentException"
}

$jsonWithEmptyKey = '{"": "Value"}'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should put variables in BeforeAll block or in It block.

It 'throw InvalidOperationException when json contains empty key name' {
$errorRecord = $null
[Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson($jsonWithEmptyKey, [ref]$errorRecord)
$errorRecord.Exception.GetType() | Should Be System.InvalidOperationException
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually we don't check an exception types. Please remove.
(For type check we use Should BeOfType)

$errorRecord = $null
[Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson($jsonWithEmptyKey, [ref]$errorRecord)
$errorRecord.Exception.GetType() | Should Be System.InvalidOperationException
$errorRecord.FullyQualifiedErrorId | Should Be EmptyKeyInJsonString
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put EmptyKeyInJsonString in quotas. It is more readable.

It 'can convert a single-line object' {
('{"a" : "1"}' | ConvertFrom-Json).a | Should Be 1

$testCasesWithAndWithoutAsHashtableSwitch = @(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put this in BeforeAll block.

@{ AsHashtable = $false }
)

It 'can convert a single-line object with AsHashtable switch set to <AsHashtable>' -TestCase $testCasesWithAndWithoutAsHashtableSwitch {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please start test descriptions with capital.

$jsonWithEmptyKey = '{"": "Value"}'
}


Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove extra line.

@iSazonov
Copy link
Collaborator

@SteveL-MSFT @TravisEz13 @dantraMSFT Could you please review?

Also should we address the comment from @powercode

Apart from object size and performance, it may matter with case sensitivity. I have run into issues where I couldn't use the PowerShell cmdlets because the json contained 'Property' and 'property'.

Removed extra newline in pester test as requested by @iSazonov
@iSazonov
Copy link
Collaborator

@bergmeister Thanks for your contribution! LGTM with above question about case sensitivity.

@SteveL-MSFT
Copy link
Member

@iSazonov @powercode regarding case sensitivity, I think we should treat it the same as the empty key. [Hashtable] allows two keys differing in case, so if we detect that, we should just tell the user to use -AsHashTable.

…found out that newtonsoft.json would squash keys that have the same casing into one and just use the last value. I still handle this case should the behaviour change.
@bergmeister
Copy link
Contributor Author

@SteveL-MSFT @iSazonov @powercode I added a specific error message to point the user to the new -AsHashTable option.
As part of this I also found out that actually no error is thrown in the case of duplicate keys with the same casing. This is due to the behaviour of Newtonsoft.Json that just uses the last item if there are multiple items with the same key of the same casing. The PowerShell code currently caters for it should this behaviour change in a newer version of Newtonsoft.Json. This should definitely be discussed in another issue if you want to make a breaking change to throw when encountering case matching duplicate keys.

Copy link
Collaborator

@iSazonov iSazonov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bergmeister Thanks for your contribution! I believe we are near to merge.
Feel free to open new Issue to discuss the discovered problem.

<value>Cannot convert the JSON string because a dictionary that was converted from the string contains the duplicated key '{0}'.</value>
</data>
<data name="KeysWithDifferentCasingInJsonString" xml:space="preserve">
<value>Cannot convert the JSON string because a PSObject does not support keys with different casing. Please use the -AsHashTable switch instead. The key that was attempted to be added to the exisiting key '{0}' was '{1}'.</value>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, very long message!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know. I shortened it a bit but I think the last sentence still adds value.

It 'Not throw when json contains key (same casing) when ReturnHashTable is true' {
$errorRecord = $null
$result = [Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson($jsonContainingKeysWithDifferentCasing, $true, [ref]$errorRecord)
$result | Should Not Be $null
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to check the hashtable values.

$errRecord = $null
[Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson($str, [ref]$errRecord)
[Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson($str, $ReturnHashTable, [ref]$errRecord)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please validate the returned object

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should capture the output as $result and validate it is what you expect

It 'Not throw when json contains empty key name when ReturnHashTable is true' {
$errorRecord = $null
$result = [Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson($jsonWithEmptyKey, $true, [ref]$errorRecord)
$result | Should Not Be $null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please validate the returned object

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved

It 'Not throw when json contains key (same casing) when ReturnHashTable is true' {
$errorRecord = $null
$result = [Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson($jsonContainingKeysWithDifferentCasing, $true, [ref]$errorRecord)
$result | Should Not Be $null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please validate the returned object

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is resolved. Thanks.

…quested in PR.

Unfortunately, Pester (3.4) does not seem to support equality assertion for hashtables...
@bergmeister
Copy link
Contributor Author

@iSazonov @SteveL-MSFT I added more validation to the positive test cases and shortened the error message a bit. I opened issue #5199 for the problem with Newtonsoft.Json for keys with matching cases, which is unrelated to this PR.

$result = [Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson($jsonWithEmptyKey, $true, [ref]$errorRecord)
$result | Should Not Be $null
$result.Count | Should Be 1
$result.$('') | Should Be 'Value'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should simplify this to just: $result."" | Should Be 'Value'

@TravisEz13 TravisEz13 merged commit f5f0d3a into PowerShell:master Oct 25, 2017
@bergmeister
Copy link
Contributor Author

@SteveL-MSFT I see that the documentation here has not been updated yet for the new -AsHashTable switch (or is it already done in another branch?). Am I supposed to do that or shall I simply open an issue in the docs repo?

@iSazonov iSazonov added the Documentation Needed in this repo Documentation is needed in this repo label Nov 14, 2017
@iSazonov
Copy link
Collaborator

@bergmeister Feel free to open an Issue in docs repo or push PR with the fix.

@SteveL-MSFT
Copy link
Member

@bergmeister preferably submit a PR to powershell-docs repo. The Documentation Needed label here means you don't need to open an issue in the powershell-docs repo. But my team won't be reviewing and adding docs until sometime after RC is out. If you submit a PR for the doc update, please reference this PR so we can remove the Documentation Needed label. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
WG-Cmdlets-Utility cmdlets in the Microsoft.PowerShell.Utility module
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants