Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ public class ConvertFromJsonCommand : Cmdlet
[Parameter]
public SwitchParameter NoEnumerate { get; set; }

/// <summary>
/// Gets or sets the switch to control how DateTime values are to be parsed as a dotnet object.
/// </summary>
[Parameter]
public JsonDateKind DateKind { get; set; } = JsonDateKind.Default;

#endregion parameters

#region overrides
Expand Down Expand Up @@ -113,7 +119,7 @@ protected override void EndProcessing()
private bool ConvertFromJsonHelper(string input)
{
ErrorRecord error = null;
object result = JsonObject.ConvertFromJson(input, AsHashtable.IsPresent, Depth, out error);
object result = JsonObject.ConvertFromJson(input, AsHashtable.IsPresent, Depth, DateKind, out error);

if (error != null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#nullable enable

namespace Microsoft.PowerShell.Commands
{
/// <summary>
/// Enums for ConvertFrom-Json -DateKind parameter.
/// </summary>
public enum JsonDateKind
{
/// <summary>
/// DateTime values are returned as a DateTime with the Kind representing the time zone in the raw string.
/// </summary>
Default,

/// <summary>
/// DateTime values are returned as the Local kind representation of the value.
/// </summary>
Local,

/// <summary>
/// DateTime values are returned as the UTC kind representation of the value.
/// </summary>
Utc,

/// <summary>
/// DateTime values are returned as a DateTimeOffset value preserving the timezone information.
/// </summary>
Offset,

/// <summary>
/// DateTime values are returned as raw strings.
/// </summary>
String,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,68 @@ public static object ConvertFromJson(string input, bool returnHashtable, out Err
/// if the <paramref name="returnHashtable"/> parameter is true.</returns>
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Justification = "Preferring Json over JSON")]
public static object ConvertFromJson(string input, bool returnHashtable, int? maxDepth, out ErrorRecord error)
=> ConvertFromJson(input, returnHashtable, maxDepth, jsonDateKind: JsonDateKind.Default, out error);

/// <summary>
/// Convert a JSON string back to an object of type <see cref="System.Management.Automation.PSObject"/> or
/// <see cref="System.Collections.Hashtable"/> depending on parameter <paramref name="returnHashtable"/>.
/// </summary>
/// <param name="input">The JSON text to convert.</param>
/// <param name="returnHashtable">True if the result should be returned as a <see cref="System.Collections.Hashtable"/>
/// instead of a <see cref="System.Management.Automation.PSObject"/>.</param>
/// <param name="maxDepth">The max depth allowed when deserializing the json input. Set to null for no maximum.</param>
/// <param name="jsonDateKind">Controls how DateTime values are to be converted.</param>
/// <param name="error">An error record if the conversion failed.</param>
/// <returns>A <see cref="System.Management.Automation.PSObject"/> or a <see cref="System.Collections.Hashtable"/>
/// if the <paramref name="returnHashtable"/> parameter is true.</returns>
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Justification = "Preferring Json over JSON")]
internal static object ConvertFromJson(string input, bool returnHashtable, int? maxDepth, JsonDateKind jsonDateKind, out ErrorRecord error)
{
ArgumentNullException.ThrowIfNull(input);

DateParseHandling dateParseHandling;
DateTimeZoneHandling dateTimeZoneHandling;
switch (jsonDateKind)
{
case JsonDateKind.Default:
dateParseHandling = DateParseHandling.DateTime;
dateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind;
break;

case JsonDateKind.Local:
dateParseHandling = DateParseHandling.DateTime;
dateTimeZoneHandling = DateTimeZoneHandling.Local;
break;

case JsonDateKind.Utc:
dateParseHandling = DateParseHandling.DateTime;
dateTimeZoneHandling = DateTimeZoneHandling.Utc;
break;

case JsonDateKind.Offset:
dateParseHandling = DateParseHandling.DateTimeOffset;
dateTimeZoneHandling = DateTimeZoneHandling.Unspecified;
break;

case JsonDateKind.String:
dateParseHandling = DateParseHandling.None;
dateTimeZoneHandling = DateTimeZoneHandling.Unspecified;
break;

default:
throw new ArgumentException($"Unknown JsonDateKind value requested '{jsonDateKind}'");
}

error = null;
try
{
var obj = JsonConvert.DeserializeObject(
input,
new JsonSerializerSettings
{
DateParseHandling = dateParseHandling,
DateTimeZoneHandling = dateTimeZoneHandling,

// This TypeNameHandling setting is required to be secure.
TypeNameHandling = TypeNameHandling.None,
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,15 @@ Describe 'ConvertFrom-Json Unit Tests' -tags "CI" {
$json | Should -BeOfType Hashtable
}
}

It 'Throws an ArgumentException with an incomplete array with AsHashtable switch set to <AsHashtable>' -TestCase $testCasesWithAndWithoutAsHashtableSwitch {
Param($AsHashtable)
{ ConvertFrom-Json '["1",' -AsHashtable:$AsHashtable } |
Should -Throw -ErrorId "System.ArgumentException,Microsoft.PowerShell.Commands.ConvertFromJsonCommand"
{ ConvertFrom-Json '[' -AsHashtable:$AsHashtable } |
Should -Throw -ErrorId "System.ArgumentException,Microsoft.PowerShell.Commands.ConvertFromJsonCommand"
}

It 'Can convert multi-line object with AsHashtable switch set to <AsHashtable>' -TestCases $testCasesWithAndWithoutAsHashtableSwitch {
Param($AsHashtable)
$json = @('{"a" :', '"x"}') | ConvertFrom-Json -AsHashtable:$AsHashtable
Expand Down Expand Up @@ -161,6 +161,195 @@ b 2
c 3
"@
}

It 'Parses DateKind Default strings for <Value>' -TestCases @(
@{
Value = '"2022-11-02T12:01:44.5801388+04:00"'
Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours 4)).AddTicks(8).LocalDateTime)
}
@{
Value = '"2022-11-02T12:01:44.5801388-04:00"'
Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours -4)).AddTicks(8).LocalDateTime)
}
@{
Value = '"1970-01-01T00:00:00"'
Expected = ([DateTime]::new(1970, 1, 1, 0, 0, 0, 0, 0, 'Unspecified'))
}
@{
Value = '"1970-01-01T00:00:00.0000000Z"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).UtcDateTime)
}
@{
Value = '"1970-01-01T00:00:00.0000000+00:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).LocalDateTime)
}
@{
Value = '"1970-01-01T00:00:00.0000000-00:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).LocalDateTime)
}
@{
Value = '"1970-01-01T00:00:00.0000000+10:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 10)).LocalDateTime)
}
@{
Value = '"1970-01-01T00:00:00.0000000-10:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours -10)).LocalDateTime)
}
) {
param ($Value, $Expected)

$json = $Value | ConvertFrom-Json
$json | Should -BeOfType ([DateTime])
$json.Kind | Should -Be $Expected.Kind
$json | Should -Be $Expected

$json = $Value | ConvertFrom-Json -DateKind Default
$json | Should -BeOfType ([DateTime])
$json.Kind | Should -Be $Expected.Kind
$json | Should -Be $Expected
}

It 'Parses DateKind Local strings for <Value>' -TestCases @(
@{
Value = '"2022-11-02T12:01:44.5801388+04:00"'
Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours 4)).AddTicks(8).LocalDateTime)
}
@{
Value = '"2022-11-02T12:01:44.5801388-04:00"'
Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours -4)).AddTicks(8).LocalDateTime)
}
@{
Value = '"1970-01-01T00:00:00"'
Expected = ([DateTime]::new(1970, 1, 1, 0, 0, 0, 0, 0, 'Local'))
}
@{
Value = '"1970-01-01T00:00:00.0000000Z"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).LocalDateTime)
}
@{
Value = '"1970-01-01T00:00:00.0000000+00:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).LocalDateTime)
}
@{
Value = '"1970-01-01T00:00:00.0000000-00:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).LocalDateTime)
}
@{
Value = '"1970-01-01T00:00:00.0000000+10:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 10)).LocalDateTime)
}
@{
Value = '"1970-01-01T00:00:00.0000000-10:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours -10)).LocalDateTime)
}
) {
param ($Value, $Expected)

$json = $Value | ConvertFrom-Json -DateKind Local
$json | Should -BeOfType ([DateTime])
$json.Kind | Should -Be Local
$json | Should -Be $Expected
}

It 'Parses DateKind Utc strings for <Value>' -TestCases @(
@{
Value = '"2022-11-02T12:01:44.5801388+04:00"'
Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours 4)).AddTicks(8).UtcDateTime)
}
@{
Value = '"2022-11-02T12:01:44.5801388-04:00"'
Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours -4)).AddTicks(8).UtcDateTime)
}
@{
Value = '"1970-01-01T00:00:00"'
Expected = ([DateTime]::new(1970, 1, 1, 0, 0, 0, 0, 0, 'Utc'))
}
@{
Value = '"1970-01-01T00:00:00.0000000Z"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).UtcDateTime)
}
@{
Value = '"1970-01-01T00:00:00.0000000+00:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).UtcDateTime)
}
@{
Value = '"1970-01-01T00:00:00.0000000-00:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).UtcDateTime)
}
@{
Value = '"1970-01-01T00:00:00.0000000+10:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 10)).UtcDateTime)
}
@{
Value = '"1970-01-01T00:00:00.0000000-10:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours -10)).UtcDateTime)
}
) {
param ($Value, $Expected)

$json = $Value | ConvertFrom-Json -DateKind Utc
$json | Should -BeOfType ([DateTime])
$json.Kind | Should -Be Utc
$json | Should -Be $Expected
}

It 'Parses DateKind Offset strings for <Value>' -TestCases @(
@{
Value = '"2022-11-02T12:01:44.5801388+04:00"'
Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours 4)).AddTicks(8))
}
@{
Value = '"2022-11-02T12:01:44.5801388-04:00"'
Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours -4)).AddTicks(8))
}
@{
Value = '"1970-01-01T00:00:00"'
Expected = ([DateTimeOffset]::new([DateTime]::new(1970, 1, 1, 0, 0, 0, 0, 0, 'Local')))
}
@{
Value = '"1970-01-01T00:00:00.0000000Z"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)))
}
@{
Value = '"1970-01-01T00:00:00.0000000+00:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)))
}
@{
Value = '"1970-01-01T00:00:00.0000000-00:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)))
}
@{
Value = '"1970-01-01T00:00:00.0000000+10:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 10)))
}
@{
Value = '"1970-01-01T00:00:00.0000000-10:00"'
Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours -10)))
}
) {
param ($Value, $Expected)

$json = $Value | ConvertFrom-Json -DateKind Offset
$json | Should -BeOfType ([DateTimeOffset])
$json.EqualsExact($Expected) | Should -BeTrue
}

It 'Parses DateKind String strings for <Value>' -TestCases @(
@{ Value = '"2022-11-02T12:01:44.5801388+04:00"' }
@{ Value = '"2022-11-02T12:01:44.5801388-04:00"' }
@{ Value = '"1970-01-01T00:00:00"' }
@{ Value = '"1970-01-01T00:00:00.0000000Z"' }
@{ Value = '"1970-01-01T00:00:00.0000000+00:00"' }
@{ Value = '"1970-01-01T00:00:00.0000000-00:00"' }
@{ Value = '"1970-01-01T00:00:00.0000000+10:00"' }
@{ Value = '"1970-01-01T00:00:00.0000000-10:00"' }
) {
param ($Value)

$json = $Value | ConvertFrom-Json -DateKind String
$json | Should -BeOfType ([string])
$json | Should -Be $Value.Substring(1, $Value.Length - 2)
}
}

Describe 'ConvertFrom-Json -Depth Tests' -tags "Feature" {
Expand Down