diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs
index 23f0cc7c37f..42926643e45 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs
@@ -2,11 +2,12 @@
// Licensed under the MIT License.
using System;
-using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
using System.Management.Automation;
-using System.Management.Automation.Internal;
-
-using Newtonsoft.Json;
+using System.Reflection;
+using System.Runtime.ExceptionServices;
+using System.Security;
using Newtonsoft.Json.Linq;
using NJsonSchema;
@@ -15,50 +16,122 @@ namespace Microsoft.PowerShell.Commands
///
/// This class implements Test-Json command.
///
- [Cmdlet(VerbsDiagnostic.Test, "Json", HelpUri = "")]
+ [Cmdlet(VerbsDiagnostic.Test, "Json", DefaultParameterSetName = ParameterAttribute.AllParameterSets, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2096609")]
public class TestJsonCommand : PSCmdlet
{
+ private const string SchemaFileParameterSet = "SchemaFile";
+ private const string SchemaStringParameterSet = "SchemaString";
+
///
- /// An JSON to be validated.
+ /// Gets or sets JSON string to be validated.
///
[Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)]
public string Json { get; set; }
///
- /// A schema to validate the JSON against.
+ /// Gets or sets schema to validate the JSON against.
/// This is optional parameter.
/// If the parameter is absent the cmdlet only attempts to parse the JSON string.
/// If the parameter present the cmdlet attempts to parse the JSON string and
/// then validates the JSON against the schema. Before testing the JSON string,
/// the cmdlet parses the schema doing implicitly check the schema too.
///
- [Parameter(Position = 1)]
- [ValidateNotNullOrEmpty()]
+ [Parameter(Position = 1, ParameterSetName = SchemaStringParameterSet)]
+ [ValidateNotNullOrEmpty]
public string Schema { get; set; }
+ ///
+ /// Gets or sets path to the file containg schema to validate the JSON string against.
+ /// This is optional parameter.
+ ///
+ [Parameter(Position = 1, ParameterSetName = SchemaFileParameterSet)]
+ [ValidateNotNullOrEmpty]
+ public string SchemaFile { get; set; }
+
private JsonSchema _jschema;
///
- /// Prepare an JSON schema.
+ /// Process all exceptions in the AggregateException.
+ /// Unwrap TargetInvocationException if any and
+ /// rethrow inner exception without losing the stack trace.
+ ///
+ /// AggregateException to be unwrapped.
+ /// Return value is unreachable since we always rethrow.
+ private static bool UnwrapException(Exception e)
+ {
+ if (e is TargetInvocationException)
+ {
+ ExceptionDispatchInfo.Capture(e.InnerException).Throw();
+ }
+ else
+ {
+ ExceptionDispatchInfo.Capture(e).Throw();
+ }
+
+ return true;
+ }
+
+ ///
+ /// Prepare a JSON schema.
///
protected override void BeginProcessing()
{
- if (Schema != null)
+ string resolvedpath = string.Empty;
+
+ try
{
- try
+ if (Schema != null)
{
- _jschema = JsonSchema.FromJsonAsync(Schema).Result;
+ try
+ {
+ _jschema = JsonSchema.FromJsonAsync(Schema).Result;
+ }
+ catch (AggregateException ae)
+ {
+ // Even if only one exception is thrown, it is still wrapped in an AggregateException exception
+ // https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/exception-handling-task-parallel-library
+ ae.Handle(UnwrapException);
+ }
}
- catch (Exception exc)
+ else if (SchemaFile != null)
{
- Exception exception = new Exception(TestJsonCmdletStrings.InvalidJsonSchema, exc);
- ThrowTerminatingError(new ErrorRecord(exception, "InvalidJsonSchema", ErrorCategory.InvalidData, null));
+ try
+ {
+ resolvedpath = Context.SessionState.Path.GetUnresolvedProviderPathFromPSPath(SchemaFile);
+ _jschema = JsonSchema.FromFileAsync(resolvedpath).Result;
+ }
+ catch (AggregateException ae)
+ {
+ ae.Handle(UnwrapException);
+ }
}
}
+ catch (Exception e) when (
+ // Handle exceptions related to file access to provide more specific error message
+ // https://docs.microsoft.com/en-us/dotnet/standard/io/handling-io-errors
+ e is IOException ||
+ e is UnauthorizedAccessException ||
+ e is NotSupportedException ||
+ e is SecurityException
+ )
+ {
+ Exception exception = new Exception(
+ string.Format(
+ CultureInfo.CurrentUICulture,
+ TestJsonCmdletStrings.JsonSchemaFileOpenFailure,
+ resolvedpath),
+ e);
+ ThrowTerminatingError(new ErrorRecord(exception, "JsonSchemaFileOpenFailure", ErrorCategory.OpenError, resolvedpath));
+ }
+ catch (Exception e)
+ {
+ Exception exception = new Exception(TestJsonCmdletStrings.InvalidJsonSchema, e);
+ ThrowTerminatingError(new ErrorRecord(exception, "InvalidJsonSchema", ErrorCategory.InvalidData, resolvedpath));
+ }
}
///
- /// Validate an JSON.
+ /// Validate a JSON.
///
protected override void ProcessRecord()
{
diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx
index a5c8d5d24d9..ab105e47fd3 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx
+++ b/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx
@@ -126,4 +126,7 @@
The JSON is not valid with the schema.
+
+ Can not open JSON schema file: {0}
+
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1
index cf5a9438406..6ca6511cd86 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1
@@ -3,6 +3,12 @@
Describe "Test-Json" -Tags "CI" {
BeforeAll {
+ $validSchemaJsonPath = Join-Path -Path (Join-Path $PSScriptRoot -ChildPath assets) -ChildPath valid_schema_reference.json
+
+ $invalidSchemaJsonPath = Join-Path -Path (Join-Path $PSScriptRoot -ChildPath assets) -ChildPath invalid_schema_reference.json
+
+ $missingSchemaJsonPath = Join-Path -Path (Join-Path $PSScriptRoot -ChildPath assets) -ChildPath no_such_file.json
+
$validSchemaJson = @"
{
'description': 'A person',
@@ -61,40 +67,73 @@ Describe "Test-Json" -Tags "CI" {
"@
}
+ It "Missing JSON schema file doesn't exist" {
+ Test-Path -LiteralPath $missingSchemaJsonPath | Should -BeFalse
+ }
+
It "Json is valid" {
Test-Json -Json $validJson | Should -BeTrue
}
- It "Json is valid against a valid schema" {
+ It "Json is valid against a valid schema from string" {
Test-Json -Json $validJson -Schema $validSchemaJson | Should -BeTrue
}
+ It "Json is valid against a valid schema from file" {
+ Test-Json -Json $validJson -SchemaFile $validSchemaJsonPath | Should -BeTrue
+ }
+
It "Json is invalid" {
Test-Json -Json $invalidNodeInJson -ErrorAction SilentlyContinue | Should -BeFalse
}
- It "Json is invalid against a valid schema" {
+ It "Json is invalid against a valid schema from string" {
Test-Json -Json $invalidTypeInJson2 -Schema $validSchemaJson -ErrorAction SilentlyContinue | Should -BeFalse
Test-Json -Json $invalidNodeInJson -Schema $validSchemaJson -ErrorAction SilentlyContinue | Should -BeFalse
}
- It "Test-Json throw if a schema is invalid" {
+ It "Json is invalid against a valid schema from file" {
+ Test-Json -Json $invalidTypeInJson2 -SchemaFile $validSchemaJsonPath -ErrorAction SilentlyContinue | Should -BeFalse
+ Test-Json -Json $invalidNodeInJson -SchemaFile $validSchemaJsonPath -ErrorAction SilentlyContinue | Should -BeFalse
+ }
+
+ It "Test-Json throw if a schema from string is invalid" {
{ Test-Json -Json $validJson -Schema $invalidSchemaJson -ErrorAction Stop } | Should -Throw -ErrorId "InvalidJsonSchema,Microsoft.PowerShell.Commands.TestJsonCommand"
}
- It "Test-Json write an error on invalid () Json against a valid schema" -TestCases @(
+ It "Test-Json throw if a schema from file is invalid" {
+ { Test-Json -Json $validJson -SchemaFile $invalidSchemaJsonPath -ErrorAction Stop } | Should -Throw -ErrorId "InvalidJsonSchema,Microsoft.PowerShell.Commands.TestJsonCommand"
+ }
+
+ It "Test-Json throw if a path to a schema from file is invalid" {
+ { Test-Json -Json $validJson -SchemaFile $missingSchemaJsonPath -ErrorAction Stop } | Should -Throw -ErrorId "JsonSchemaFileOpenFailure,Microsoft.PowerShell.Commands.TestJsonCommand"
+ }
+
+ It "Test-Json write an error on invalid () Json against a valid schema from string" -TestCases @(
@{ name = "type"; json = $invalidTypeInJson; errorId = "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" }
@{ name = "node"; json = $invalidNodeInJson; errorId = "InvalidJson,Microsoft.PowerShell.Commands.TestJsonCommand" }
- ) {
- param ($json, $errorId)
+ ) {
+ param ($json, $errorId)
- $errorVar = $null
- Test-Json -Json $json -Schema $validSchemaJson -ErrorVariable errorVar -ErrorAction SilentlyContinue
+ $errorVar = $null
+ Test-Json -Json $json -Schema $validSchemaJson -ErrorVariable errorVar -ErrorAction SilentlyContinue
- $errorVar.FullyQualifiedErrorId | Should -BeExactly $errorId
+ $errorVar.FullyQualifiedErrorId | Should -BeExactly $errorId
}
- It "Test-Json return all errors when check invalid Json against a valid schema" {
+ It "Test-Json write an error on invalid () Json against a valid schema from file" -TestCases @(
+ @{ name = "type"; json = $invalidTypeInJson; errorId = "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" }
+ @{ name = "node"; json = $invalidNodeInJson; errorId = "InvalidJson,Microsoft.PowerShell.Commands.TestJsonCommand" }
+ ) {
+ param ($json, $errorId)
+
+ $errorVar = $null
+ Test-Json -Json $json -SchemaFile $validSchemaJsonPath -ErrorVariable errorVar -ErrorAction SilentlyContinue
+
+ $errorVar.FullyQualifiedErrorId | Should -BeExactly $errorId
+ }
+
+ It "Test-Json return all errors when check invalid Json against a valid schema from string" {
$errorVar = $null
Test-Json -Json $invalidTypeInJson2 -Schema $validSchemaJson -ErrorVariable errorVar -ErrorAction SilentlyContinue
@@ -103,4 +142,14 @@ Describe "Test-Json" -Tags "CI" {
$errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand"
$errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand"
}
+
+ It "Test-Json return all errors when check invalid Json against a valid schema from file" {
+ $errorVar = $null
+ Test-Json -Json $invalidTypeInJson2 -SchemaFile $validSchemaJsonPath -ErrorVariable errorVar -ErrorAction SilentlyContinue
+
+ # '$invalidTypeInJson2' contains two errors in property types.
+ $errorVar.Count | Should -Be 2
+ $errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand"
+ $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand"
+ }
}
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/invalid_schema_definitions.json b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/invalid_schema_definitions.json
new file mode 100644
index 00000000000..d3fc0cdeef9
--- /dev/null
+++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/invalid_schema_definitions.json
@@ -0,0 +1,8 @@
+{
+ "definitions": {
+ "name": {
+ "type": "string"
+ },
+ "hobbies"
+ }
+}
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/invalid_schema_reference.json b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/invalid_schema_reference.json
new file mode 100644
index 00000000000..32520f59496
--- /dev/null
+++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/invalid_schema_reference.json
@@ -0,0 +1,12 @@
+{
+ "description": "A person",
+ "type": "object",
+ "properties": {
+ "name": {
+ "$ref": "invalid_schema_definitions.json#/definitions/name"
+ },
+ "hobbies": {
+ "$ref": "invalid_schema_definitions.json#/definitions/hobbies"
+ }
+ }
+}
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/valid_schema_definitions.json b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/valid_schema_definitions.json
new file mode 100644
index 00000000000..5396927a5bf
--- /dev/null
+++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/valid_schema_definitions.json
@@ -0,0 +1,13 @@
+{
+ "definitions": {
+ "name": {
+ "type": "string"
+ },
+ "hobbies": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+}
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/valid_schema_reference.json b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/valid_schema_reference.json
new file mode 100644
index 00000000000..aa9c18a30c7
--- /dev/null
+++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/valid_schema_reference.json
@@ -0,0 +1,12 @@
+{
+ "description": "A person",
+ "type": "object",
+ "properties": {
+ "name": {
+ "$ref": "valid_schema_definitions.json#/definitions/name"
+ },
+ "hobbies": {
+ "$ref": "valid_schema_definitions.json#/definitions/hobbies"
+ }
+ }
+}