Test-Json: Use JsonSchema.Net (System.Text.Json) instead of NJsonSchema (Newtonsoft.Json) (#18141)

Updates the Test-Json cmdlet to use JsonSchema.Net instead of NJsonSchema in order to:

- use System.Text.Json instead of Newtonsoft
- support the latest JSON Schema drafts (up to 2020-12)
This commit is contained in:
Greg Dennis
2023-05-04 08:19:32 +05:00
committed by GitHub
parent aa9a88fc33
commit 1b248efb3f
8 changed files with 182 additions and 123 deletions
@@ -35,11 +35,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0-1.final" />
<PackageReference Include="System.Threading.AccessControl" Version="8.0.0-preview.3.23174.8" />
<PackageReference Include="System.Drawing.Common" Version="8.0.0-preview.2.23128.3" />
<PackageReference Include="NJsonSchema" Version="10.8.0" />
<!-- NJsonSchema brings in an old version of CSharp
So, we are forcing a newer version.
-->
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="JsonSchema.Net" Version="3.3.2" />
</ItemGroup>
</Project>
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System;
namespace Microsoft.PowerShell.Commands;
/// <summary>
/// Thrown during evaluation of <see cref="TestJsonCommand"/> when an attempt
/// to resolve a <code>$ref</code> or <code>$dynamicRef</code> fails.
/// </summary>
internal class JsonSchemaReferenceResolutionException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonSchemaReferenceResolutionException"/> class.
/// </summary>
/// <param name="innerException">
/// The exception that is the cause of the current exception, or a null reference
/// (<code>Nothing</code> in Visual Basic) if no inner exception is specified.
/// </param>
public JsonSchemaReferenceResolutionException(Exception innerException)
: base(message: null, innerException)
{
}
}
@@ -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;
/// <summary>
/// Process all exceptions in the AggregateException.
/// Unwrap TargetInvocationException if any and
/// rethrow inner exception without losing the stack trace.
/// </summary>
/// <param name="e">AggregateException to be unwrapped.</param>
/// <returns>Return value is unreachable since we always rethrow.</returns>
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
/// <summary>
/// Prepare a JSON schema.
/// </summary>
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<JsonSchema>(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
}
}
@@ -123,10 +123,13 @@
<data name="InvalidJson" xml:space="preserve">
<value>Cannot parse the JSON.</value>
</data>
<data name="InvalidJsonAgainstSchema" xml:space="preserve">
<value>The JSON is not valid with the schema.</value>
<data name="InvalidJsonAgainstSchemaDetailed" xml:space="preserve">
<value>The JSON is not valid with the schema: {0} at {1}</value>
</data>
<data name="JsonSchemaFileOpenFailure" xml:space="preserve">
<value>Can not open JSON schema file: {0}</value>
</data>
<data name="InvalidUriScheme" xml:space="preserve">
<value>URI scheme '{0}' is not supported. Only HTTP(S) and local file system URIs are allowed.</value>
</data>
</root>
@@ -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 (<name>) 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 (<name>) 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 (<name>) 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 (<name>) 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: <name>' -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
}
+1 -1
View File
@@ -492,7 +492,7 @@
"FileType": "NonProduct"
},
{
"Pattern": "NJsonSchema.dll",
"Pattern": "JsonSchema.Net.dll",
"FileType": "NonProduct"
},
{
+1 -1
View File
@@ -372,7 +372,7 @@
"FileType": "NonProduct"
},
{
"Pattern": "NJsonSchema.dll",
"Pattern": "JsonSchema.Net.dll",
"FileType": "NonProduct"
},
{
+16 -8
View File
@@ -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"