diff --git a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj
index 085f7cf63b..ea5e0bd22a 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj
+++ b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj
@@ -35,11 +35,7 @@
-
-
-
+
diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/JsonSchemaReferenceResolutionException.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/JsonSchemaReferenceResolutionException.cs
new file mode 100644
index 0000000000..ba664ccef1
--- /dev/null
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/JsonSchemaReferenceResolutionException.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#nullable enable
+
+using System;
+
+namespace Microsoft.PowerShell.Commands;
+
+///
+/// Thrown during evaluation of when an attempt
+/// to resolve a $ref or $dynamicRef fails.
+///
+internal class JsonSchemaReferenceResolutionException : Exception
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The exception that is the cause of the current exception, or a null reference
+ /// (Nothing in Visual Basic) if no inner exception is specified.
+ ///
+ public JsonSchemaReferenceResolutionException(Exception innerException)
+ : base(message: null, innerException)
+ {
+ }
+}
diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs
index fc416d9772..4a6e11a963 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs
@@ -5,11 +5,11 @@ using System;
using System.Globalization;
using System.IO;
using System.Management.Automation;
-using System.Reflection;
-using System.Runtime.ExceptionServices;
+using System.Net.Http;
using System.Security;
-using Newtonsoft.Json.Linq;
-using NJsonSchema;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Json.Schema;
namespace Microsoft.PowerShell.Commands
{
@@ -104,36 +104,50 @@ namespace Microsoft.PowerShell.Commands
private bool _isLiteralPath = false;
private JsonSchema _jschema;
- ///
- /// 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.InnerException != null && e is TargetInvocationException)
- {
- ExceptionDispatchInfo.Capture(e.InnerException).Throw();
- }
- else
- {
- ExceptionDispatchInfo.Capture(e).Throw();
- }
-
- return true;
- }
-
#endregion
- #region Protected Members
-
///
/// Prepare a JSON schema.
///
protected override void BeginProcessing()
{
+ // By default, a JSON Schema implementation isn't supposed to automatically fetch content.
+ // Instead JsonSchema.Net has been set up with a registry so that users can pre-register
+ // any schemas they may need to resolve.
+ // However, pre-registering schemas doesn't make sense in the context of a Powershell command,
+ // and automatically fetching referenced URIs is likely the preferred behavior. To do that,
+ // this property must be set with a method to retrieve and deserialize the content.
+ // For more information, see https://json-everything.net/json-schema#automatic-resolution
+ SchemaRegistry.Global.Fetch = static uri =>
+ {
+ try
+ {
+ string text;
+ switch (uri.Scheme)
+ {
+ case "http":
+ case "https":
+ {
+ using var client = new HttpClient();
+ text = client.GetStringAsync(uri).Result;
+ break;
+ }
+ case "file":
+ var filename = Uri.UnescapeDataString(uri.AbsolutePath);
+ text = File.ReadAllText(filename);
+ break;
+ default:
+ throw new FormatException(string.Format(TestJsonCmdletStrings.InvalidUriScheme, uri.Scheme));
+ }
+
+ return JsonSerializer.Deserialize(text);
+ }
+ catch (Exception e)
+ {
+ throw new JsonSchemaReferenceResolutionException(e);
+ }
+ };
+
string resolvedpath = string.Empty;
try
@@ -142,13 +156,12 @@ namespace Microsoft.PowerShell.Commands
{
try
{
- _jschema = JsonSchema.FromJsonAsync(Schema).Result;
+ _jschema = JsonSchema.FromText(Schema);
}
- catch (AggregateException ae)
+ catch (JsonException e)
{
- // 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);
+ Exception exception = new(TestJsonCmdletStrings.InvalidJsonSchema, e);
+ WriteError(new ErrorRecord(exception, "InvalidJsonSchema", ErrorCategory.InvalidData, Schema));
}
}
else if (SchemaFile != null)
@@ -156,11 +169,12 @@ namespace Microsoft.PowerShell.Commands
try
{
resolvedpath = Context.SessionState.Path.GetUnresolvedProviderPathFromPSPath(SchemaFile);
- _jschema = JsonSchema.FromFileAsync(resolvedpath).Result;
+ _jschema = JsonSchema.FromFile(resolvedpath);
}
- catch (AggregateException ae)
+ catch (JsonException e)
{
- ae.Handle(UnwrapException);
+ Exception exception = new(TestJsonCmdletStrings.InvalidJsonSchema, e);
+ WriteError(new ErrorRecord(exception, "InvalidJsonSchema", ErrorCategory.InvalidData, SchemaFile));
}
}
}
@@ -194,6 +208,7 @@ namespace Microsoft.PowerShell.Commands
protected override void ProcessRecord()
{
bool result = true;
+
string jsonToParse = string.Empty;
if (Json != null)
@@ -217,45 +232,57 @@ namespace Microsoft.PowerShell.Commands
jsonToParse = File.ReadAllText(resolvedPath);
}
- if (string.IsNullOrWhiteSpace(jsonToParse))
- {
- WriteObject(false);
- return;
- }
-
try
{
- var parsedJson = JToken.Parse(jsonToParse);
+
+ var parsedJson = JsonNode.Parse(jsonToParse);
if (_jschema != null)
{
- var errorMessages = _jschema.Validate(parsedJson);
- if (errorMessages != null && errorMessages.Count != 0)
- {
- result = false;
-
- Exception exception = new(TestJsonCmdletStrings.InvalidJsonAgainstSchema);
-
- foreach (var message in errorMessages)
+ var validationResults = _jschema.Validate(parsedJson, new ValidationOptions { OutputFormat = OutputFormat.Basic });
+ result = validationResults.IsValid;
+ if (!result)
{
- ErrorRecord errorRecord = new(exception, "InvalidJsonAgainstSchema", ErrorCategory.InvalidData, null);
- errorRecord.ErrorDetails = new ErrorDetails(message.ToString());
- WriteError(errorRecord);
+ if (validationResults.Message != null)
+ {
+ Exception exception = new(string.Format(TestJsonCmdletStrings.InvalidJsonAgainstSchemaDetailed, validationResults.Message, validationResults.InstanceLocation));
+ ErrorRecord errorRecord = new(exception, "InvalidJsonAgainstSchemaDetailed", ErrorCategory.InvalidData, null);
+ WriteError(errorRecord);
+ }
+
+ if (validationResults.HasNestedResults)
+ {
+ foreach (var nestedResult in validationResults.NestedResults)
+ {
+ if (nestedResult.Message == null)
+ {
+ continue;
+ }
+
+ Exception exception = new(string.Format(TestJsonCmdletStrings.InvalidJsonAgainstSchemaDetailed, nestedResult.Message, nestedResult.InstanceLocation));
+ ErrorRecord errorRecord = new(exception, "InvalidJsonAgainstSchemaDetailed", ErrorCategory.InvalidData, null);
+ WriteError(errorRecord);
+ }
+ }
}
- }
}
}
+ catch (JsonSchemaReferenceResolutionException jsonExc)
+ {
+ result = false;
+
+ Exception exception = new(TestJsonCmdletStrings.InvalidJsonSchema, jsonExc);
+ WriteError(new ErrorRecord(exception, "InvalidJsonSchema", ErrorCategory.InvalidData, _jschema));
+ }
catch (Exception exc)
{
result = false;
Exception exception = new(TestJsonCmdletStrings.InvalidJson, exc);
- WriteError(new ErrorRecord(exception, "InvalidJson", ErrorCategory.InvalidData, jsonToParse));
+ WriteError(new ErrorRecord(exception, "InvalidJson", ErrorCategory.InvalidData, Json));
}
WriteObject(result);
}
-
- #endregion
}
}
diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx
index ab105e47fd..eed85d28b3 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx
+++ b/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx
@@ -123,10 +123,13 @@
Cannot parse the JSON.
-
- The JSON is not valid with the schema.
+
+ The JSON is not valid with the schema: {0} at {1}
Can not open JSON schema file: {0}
+
+ URI scheme '{0}' is not supported. Only HTTP(S) and local file system URIs are allowed.
+
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 9e949e0215..a5aa52e029 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1
@@ -9,62 +9,62 @@ Describe "Test-Json" -Tags "CI" {
$missingSchemaJsonPath = Join-Path -Path $assetsPath -ChildPath no_such_file.json
$missingJsonPath = Join-Path -Path $assetsPath -ChildPath no_such_file.json
- $validSchemaJson = @"
+ $validSchemaJson = @'
{
- 'description': 'A person',
- 'type': 'object',
- 'properties': {
- 'name': {'type': 'string'},
- 'hobbies': {
- 'type': 'array',
- 'items': {'type': 'string'}
+ "description": "A person",
+ "type": "object",
+ "properties": {
+ "name": {"type": "string"},
+ "hobbies": {
+ "type": "array",
+ "items": {"type": "string"}
}
}
}
-"@
+'@
- $invalidSchemaJson = @"
+ $invalidSchemaJson = @'
{
- 'description',
- 'type': 'object',
- 'properties': {
- 'name': {'type': 'string'},
- 'hobbies': {
- 'type': 'array',
- 'items': {'type': 'string'}
+ "description",
+ "type": "object",
+ "properties": {
+ "name": {"type": "string"},
+ "hobbies": {
+ "type": "array",
+ "items": {"type": "string"}
}
}
}
-"@
+'@
- $validJson = @"
+ $validJson = @'
{
- 'name': 'James',
- 'hobbies': ['.NET', 'Blogging', 'Reading', 'Xbox', 'LOLCATS']
+ "name": "James",
+ "hobbies": [".NET", "Blogging", "Reading", "Xbox", "LOLCATS"]
}
-"@
+'@
- $invalidTypeInJson = @"
+ $invalidTypeInJson = @'
{
- 'name': 123,
- 'hobbies': ['.NET', 'Blogging', 'Reading', 'Xbox', 'LOLCATS']
+ "name": 123,
+ "hobbies": [".NET", "Blogging", "Reading", "Xbox", "LOLCATS"]
}
-"@
+'@
- $invalidTypeInJson2 = @"
+ $invalidTypeInJson2 = @'
{
- 'name': 123,
- 'hobbies': [456, 'Blogging', 'Reading', 'Xbox', 'LOLCATS']
+ "name": 123,
+ "hobbies": [456, "Blogging", "Reading", "Xbox", "LOLCATS"]
}
-"@
+'@
- $invalidNodeInJson = @"
+ $invalidNodeInJson = @'
{
- 'name': 'James',
- 'hobbies': ['.NET', 'Blogging', 'Reading', 'Xbox', 'LOLCATS']
+ "name": "James",
+ "hobbies": [".NET", "Blogging", "Reading", "Xbox", "LOLCATS"]
errorNode
}
-"@
+'@
$validJsonPath = Join-Path -Path $TestDrive -ChildPath 'validJson.json'
$validLiteralJsonPath = Join-Path -Path $TestDrive -ChildPath "[valid]Json.json"
@@ -91,12 +91,10 @@ Describe "Test-Json" -Tags "CI" {
It "Json is valid" {
Test-Json -Json $validJson | Should -BeTrue
- ($validJson | Test-Json) | Should -BeTrue
}
It "Json is valid against a valid schema from string" {
Test-Json -Json $validJson -Schema $validSchemaJson | Should -BeTrue
- ($validJson | Test-Json -Schema $validSchemaJson) | Should -BeTrue
}
It "Json is valid against a valid schema from file" {
@@ -201,7 +199,7 @@ Describe "Test-Json" -Tags "CI" {
}
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 = "type"; json = $invalidTypeInJson; errorId = "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" }
@{ name = "node"; json = $invalidNodeInJson; errorId = "InvalidJson,Microsoft.PowerShell.Commands.TestJsonCommand" }
) {
param ($json, $errorId)
@@ -213,7 +211,7 @@ Describe "Test-Json" -Tags "CI" {
}
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 = "type"; json = $invalidTypeInJson; errorId = "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" }
@{ name = "node"; json = $invalidNodeInJson; errorId = "InvalidJson,Microsoft.PowerShell.Commands.TestJsonCommand" }
) {
param ($json, $errorId)
@@ -225,7 +223,7 @@ Describe "Test-Json" -Tags "CI" {
}
It "Test-Json write an error on invalid () Json file against a valid schema from string" -TestCases @(
- @{ name = "type"; json = $invalidTypeInJsonPath; errorId = "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" }
+ @{ name = "type"; json = $invalidTypeInJsonPath; errorId = "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" }
@{ name = "node"; json = $invalidNodeInJsonPath; errorId = "InvalidJson,Microsoft.PowerShell.Commands.TestJsonCommand" }
) {
param ($json, $errorId)
@@ -237,7 +235,7 @@ Describe "Test-Json" -Tags "CI" {
}
It "Test-Json write an error on invalid () Json file against a valid schema from file" -TestCases @(
- @{ name = "type"; json = $invalidTypeInJsonPath; errorId = "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" }
+ @{ name = "type"; json = $invalidTypeInJsonPath; errorId = "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" }
@{ name = "node"; json = $invalidNodeInJsonPath; errorId = "InvalidJson,Microsoft.PowerShell.Commands.TestJsonCommand" }
) {
param ($json, $errorId)
@@ -254,8 +252,8 @@ Describe "Test-Json" -Tags "CI" {
# '$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"
+ $errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand"
+ $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand"
}
It "Test-Json return all errors when check invalid Json against a valid schema from file" {
@@ -264,8 +262,8 @@ Describe "Test-Json" -Tags "CI" {
# '$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"
+ $errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand"
+ $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand"
}
It "Test-Json return all errors when check invalid Json file against a valid schema from string" {
@@ -274,8 +272,8 @@ Describe "Test-Json" -Tags "CI" {
# '$invalidTypeInJson2Path' 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"
+ $errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand"
+ $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand"
}
It "Test-Json return all errors when check invalid Json file against a valid schema from file" {
@@ -284,8 +282,8 @@ Describe "Test-Json" -Tags "CI" {
# '$invalidTypeInJson2Path' 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"
+ $errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand"
+ $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand"
}
It 'Test-Json recognizes non-object types: ' -TestCases @(
@@ -307,7 +305,7 @@ Describe "Test-Json" -Tags "CI" {
# Exactly one type should match
$types = 'string', 'number', 'boolean', 'null', 'array', 'object'
$types | Where-Object {
- $schema = "{ 'type': '$_' }"
+ $schema = "{ `"type`": `"$_`" }"
Test-Json -Json $value -Schema $schema -ErrorAction SilentlyContinue
} | Should -Be $expected
}
diff --git a/tools/packaging/boms/linux.json b/tools/packaging/boms/linux.json
index b115e72b55..f735ea5cf1 100644
--- a/tools/packaging/boms/linux.json
+++ b/tools/packaging/boms/linux.json
@@ -492,7 +492,7 @@
"FileType": "NonProduct"
},
{
- "Pattern": "NJsonSchema.dll",
+ "Pattern": "JsonSchema.Net.dll",
"FileType": "NonProduct"
},
{
diff --git a/tools/packaging/boms/mac.json b/tools/packaging/boms/mac.json
index 28593cdd14..29f2bb8532 100644
--- a/tools/packaging/boms/mac.json
+++ b/tools/packaging/boms/mac.json
@@ -372,7 +372,7 @@
"FileType": "NonProduct"
},
{
- "Pattern": "NJsonSchema.dll",
+ "Pattern": "JsonSchema.Net.dll",
"FileType": "NonProduct"
},
{
diff --git a/tools/packaging/boms/windows.json b/tools/packaging/boms/windows.json
index dc4329e475..747fa838b5 100644
--- a/tools/packaging/boms/windows.json
+++ b/tools/packaging/boms/windows.json
@@ -555,6 +555,22 @@
"Pattern": "ja/WindowsFormsIntegration.resources.dll",
"FileType": "NonProduct"
},
+ {
+ "Pattern": "JetBrains.Annotations.dll",
+ "FileType": "NonProduct"
+ },
+ {
+ "Pattern": "Json.More.dll",
+ "FileType": "NonProduct"
+ },
+ {
+ "Pattern": "JsonPointer.Net.dll",
+ "FileType": "NonProduct"
+ },
+ {
+ "Pattern": "JsonSchema.Net.dll",
+ "FileType": "NonProduct"
+ },
{
"Pattern": "ko/Microsoft.CodeAnalysis.CSharp.resources.dll",
"FileType": "NonProduct"
@@ -871,10 +887,6 @@
"Pattern": "msquic.dll",
"FileType": "NonProduct"
},
- {
- "Pattern": "Namotion.Reflection.dll",
- "FileType": "NonProduct"
- },
{
"Pattern": "netstandard.dll",
"FileType": "NonProduct"
@@ -883,10 +895,6 @@
"Pattern": "Newtonsoft.Json.dll",
"FileType": "NonProduct"
},
- {
- "Pattern": "NJsonSchema.dll",
- "FileType": "NonProduct"
- },
{
"Pattern": "PenImc_cor3.dll",
"FileType": "NonProduct"