Skip to content
Closed
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 @@ -76,6 +76,15 @@ public int Depth
[Parameter]
public StringEscapeHandling EscapeHandling { get; set; } = StringEscapeHandling.Default;

/// <summary>
/// Gets or sets the SkipUnsupportedTypes property.
/// If the SkipUnsupportedTypes property is set to true, properties that cannot be
/// converted to JSON will be silently skipped. Otherwise, an exception will be thrown
/// when such properties are encountered.
/// </summary>
[Parameter]
public SwitchParameter SkipUnsupportedTypes { get; set; }

/// <summary>
/// IDisposable implementation, dispose of any disposable resources created by the cmdlet.
/// </summary>
Expand Down Expand Up @@ -123,6 +132,7 @@ protected override void EndProcessing()
EnumsAsStrings.IsPresent,
Compress.IsPresent,
EscapeHandling,
SkipUnsupportedTypes,
targetCmdlet: this,
_cancellationSource.Token);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,19 @@ public readonly struct ConvertToJsonContext
/// </summary>
public readonly PSCmdlet Cmdlet;

/// <summary>
/// Gets the SkipUnsupportedTypes setting.
/// </summary>
public readonly bool SkipUnsupportedTypes;

/// <summary>
/// Initializes a new instance of the <see cref="ConvertToJsonContext"/> struct.
/// </summary>
/// <param name="maxDepth">The maximum depth to visit the object.</param>
/// <param name="enumsAsStrings">Indicates whether to use enum names for the JSON conversion.</param>
/// <param name="compressOutput">Indicates whether to get the compressed output.</param>
public ConvertToJsonContext(int maxDepth, bool enumsAsStrings, bool compressOutput)
: this(maxDepth, enumsAsStrings, compressOutput, StringEscapeHandling.Default, targetCmdlet: null, CancellationToken.None)
: this(maxDepth, enumsAsStrings, compressOutput, StringEscapeHandling.Default, skipUnsupportedTypes: false, targetCmdlet: null, CancellationToken.None)
{
}

Expand All @@ -88,13 +93,36 @@ public ConvertToJsonContext(
StringEscapeHandling stringEscapeHandling,
PSCmdlet targetCmdlet,
CancellationToken cancellationToken)
: this(maxDepth, enumsAsStrings, compressOutput, stringEscapeHandling, skipUnsupportedTypes: false, targetCmdlet, cancellationToken)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ConvertToJsonContext"/> struct.
/// </summary>
/// <param name="maxDepth">The maximum depth to visit the object.</param>
/// <param name="enumsAsStrings">Indicates whether to use enum names for the JSON conversion.</param>
/// <param name="compressOutput">Indicates whether to get the compressed output.</param>
/// <param name="stringEscapeHandling">Specifies how strings are escaped when writing JSON text.</param>
/// <param name="skipUnsupportedTypes">Indicates whether to skip properties that cannot be converted to JSON.</param>
/// <param name="targetCmdlet">Specifies the cmdlet that is calling this method.</param>
/// <param name="cancellationToken">Specifies the cancellation token for cancelling the operation.</param>
public ConvertToJsonContext(
int maxDepth,
bool enumsAsStrings,
bool compressOutput,
StringEscapeHandling stringEscapeHandling,
bool skipUnsupportedTypes,
PSCmdlet targetCmdlet,
CancellationToken cancellationToken)
{
this.MaxDepth = maxDepth;
this.CancellationToken = cancellationToken;
this.StringEscapeHandling = stringEscapeHandling;
this.EnumsAsStrings = enumsAsStrings;
this.CompressOutput = compressOutput;
this.Cmdlet = targetCmdlet;
this.SkipUnsupportedTypes = skipUnsupportedTypes;
}
}

Expand Down Expand Up @@ -749,6 +777,12 @@ private static object ProcessDictionary(IDictionary dict, int depth, in ConvertT
string name = entry.Key as string;
if (name == null)
{
if (context.SkipUnsupportedTypes)
{
// Skip this entry if SkipUnsupportedTypes is enabled
continue;
}

// use the error string that matches the message from JavaScriptSerializer
string errorMsg = string.Format(
CultureInfo.CurrentCulture,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,181 @@ Describe 'ConvertTo-Json' -tags "CI" {
$actual = ConvertTo-Json -Compress -InputObject $obj
$actual | Should -Be '{"Positive":18446744073709551615,"Negative":-18446744073709551615}'
}

Context 'SkipUnsupportedTypes parameter' {
It 'Should throw error for dictionary with non-string keys without SkipUnsupportedTypes' {
$dict = @{ 1 = 'one'; 2 = 'two' }
{ $dict | ConvertTo-Json } | Should -Throw -ErrorId 'NonStringKeyInDictionary,Microsoft.PowerShell.Commands.ConvertToJsonCommand'
}

It 'Should skip dictionary with non-string keys with SkipUnsupportedTypes' {
$dict = @{ 1 = 'one'; 2 = 'two' }
$json = $dict | ConvertTo-Json -SkipUnsupportedTypes
# The result should be an empty object since all keys were skipped
$json | ConvertFrom-Json | Get-Member -MemberType NoteProperty | Should -HaveCount 0
}

It 'Should throw error for Exception.Data without SkipUnsupportedTypes' {
try {
1/0
} catch {
$err = $_
}
{ $err | ConvertTo-Json -Depth 2 -WarningAction SilentlyContinue } | Should -Throw -ErrorId 'NonStringKeyInDictionary,Microsoft.PowerShell.Commands.ConvertToJsonCommand'
}

It 'Should convert Exception with SkipUnsupportedTypes' {
try {
1/0
} catch {
$err = $_
}
$json = $err | ConvertTo-Json -Depth 2 -SkipUnsupportedTypes -WarningAction SilentlyContinue
$json | Should -Not -BeNullOrEmpty
$result = $json | ConvertFrom-Json
$result.Exception | Should -Not -BeNullOrEmpty
$result.Exception.Message | Should -Be 'Attempted to divide by zero.'
}

It 'Should convert mixed dictionary with string and non-string keys' {
$dict = @{ 'key1' = 'value1'; 1 = 'value2'; 'key2' = 'value3' }
$json = $dict | ConvertTo-Json -SkipUnsupportedTypes
$result = $json | ConvertFrom-Json
# Only string keys should be preserved
$result.key1 | Should -Be 'value1'
$result.key2 | Should -Be 'value3'
# Non-string key should be skipped
$result.PSObject.Properties.Name | Should -Not -Contain '1'
}

It 'Should work normally for objects with string keys only' {
$obj = [PSCustomObject]@{ Name = 'Test'; Value = 123 }
$json1 = $obj | ConvertTo-Json
$json2 = $obj | ConvertTo-Json -SkipUnsupportedTypes
$json1 | Should -Be $json2
}

It 'Should work for nested objects with unsupported types' {
$nested = @{
'outer' = 'value1'
'inner' = @{
'string_key' = 'value2'
1 = 'should_be_skipped'
}
}
$json = $nested | ConvertTo-Json -Depth 3 -SkipUnsupportedTypes
$result = $json | ConvertFrom-Json
$result.outer | Should -Be 'value1'
$result.inner.string_key | Should -Be 'value2'
$result.inner.PSObject.Properties.Name | Should -Not -Contain '1'
}

It 'Should throw error when -SkipUnsupportedTypes:$false is explicitly specified' {
$dict = @{ 1 = 'one'; 2 = 'two' }
{ $dict | ConvertTo-Json -SkipUnsupportedTypes:$false } | Should -Throw -ErrorId 'NonStringKeyInDictionary,Microsoft.PowerShell.Commands.ConvertToJsonCommand'
}

It 'Should behave the same with -SkipUnsupportedTypes:$false and without parameter' {
$dict = @{ 1 = 'one'; 2 = 'two' }
# Both should throw the same error
$error1 = $null
$error2 = $null
try { $dict | ConvertTo-Json } catch { $error1 = $_ }
try { $dict | ConvertTo-Json -SkipUnsupportedTypes:$false } catch { $error2 = $_ }
$error1.Exception.Message | Should -Be $error2.Exception.Message
$error1.FullyQualifiedErrorId | Should -Be $error2.FullyQualifiedErrorId
}

# Backward compatibility tests
It 'Should maintain backward compatibility - string key dictionaries work without changes' {
# This test ensures existing scripts continue to work
$dict = @{ 'key1' = 'value1'; 'key2' = 'value2' }
$json = $dict | ConvertTo-Json
$result = $json | ConvertFrom-Json
$result.key1 | Should -Be 'value1'
$result.key2 | Should -Be 'value2'
}

It 'Should maintain backward compatibility - default error behavior unchanged' {
# This test ensures the default behavior (throwing error) is unchanged
# when SkipUnsupportedTypes is not specified
$dict = @{ 1 = 'one' }
{ $dict | ConvertTo-Json } | Should -Throw -ErrorId 'NonStringKeyInDictionary,Microsoft.PowerShell.Commands.ConvertToJsonCommand'
}

It 'Should maintain backward compatibility - complex objects work without changes' {
# This test ensures existing complex object serialization still works
$obj = [PSCustomObject]@{
Name = 'Test'
Value = 123
Nested = @{
'inner' = 'data'
}
}
$json = $obj | ConvertTo-Json -Depth 3
$result = $json | ConvertFrom-Json
$result.Name | Should -Be 'Test'
$result.Value | Should -Be 123
$result.Nested.inner | Should -Be 'data'
}

# Constructor backward compatibility tests
It 'Should allow calling 3-parameter constructor (backward compatibility)' {
# This tests that existing code using the 3-parameter constructor still works
$context = [Microsoft.PowerShell.Commands.JsonObject+ConvertToJsonContext]::new(10, $true, $false)
$context.MaxDepth | Should -Be 10
$context.EnumsAsStrings | Should -BeTrue
$context.CompressOutput | Should -BeFalse
}

It 'Should allow calling 6-parameter constructor (backward compatibility)' {
# This tests that existing code using the 6-parameter constructor still works
$context = [Microsoft.PowerShell.Commands.JsonObject+ConvertToJsonContext]::new(
5,
$false,
$true,
[Newtonsoft.Json.StringEscapeHandling]::Default,
$null,
[System.Threading.CancellationToken]::None
)
$context.MaxDepth | Should -Be 5
$context.EnumsAsStrings | Should -BeFalse
$context.CompressOutput | Should -BeTrue
}

It 'Should allow calling 7-parameter constructor with SkipUnsupportedTypes' {
# This tests the new 7-parameter constructor
$context = [Microsoft.PowerShell.Commands.JsonObject+ConvertToJsonContext]::new(
3,
$true,
$false,
[Newtonsoft.Json.StringEscapeHandling]::EscapeHtml,
$true,
$null,
[System.Threading.CancellationToken]::None
)
$context.MaxDepth | Should -Be 3
$context.EnumsAsStrings | Should -BeTrue
$context.CompressOutput | Should -BeFalse
$context.SkipUnsupportedTypes | Should -BeTrue
}

# New feature tests (SkipUnsupportedTypes field)
It 'Should have SkipUnsupportedTypes default to false in 3-parameter constructor' {
$context = [Microsoft.PowerShell.Commands.JsonObject+ConvertToJsonContext]::new(10, $true, $false)
$context.SkipUnsupportedTypes | Should -Be $false -Because 'SkipUnsupportedTypes should default to false'
}

It 'Should have SkipUnsupportedTypes default to false in 6-parameter constructor' {
$context = [Microsoft.PowerShell.Commands.JsonObject+ConvertToJsonContext]::new(
5,
$false,
$true,
[Newtonsoft.Json.StringEscapeHandling]::Default,
$null,
[System.Threading.CancellationToken]::None
)
$context.SkipUnsupportedTypes | Should -Be $false -Because 'SkipUnsupportedTypes should default to false'
}
}
}
Loading