Add Test-Json cmdlet (NJsonSchema) (#5229)
Resolve #4220. The cmdlet is based on NJsonSchema. It allows to check: JSON by only parsing JSON against Schema implicitly check Schema by parsing (based on previous line check) NJsonSchema is under MIT (approved see https://github.com/PowerShell/PowerShell/pull/5229#issuecomment-342983215)
This commit is contained in:
parent
a29484051e
commit
1b745f6a98
|
@ -76,3 +76,30 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
-------------------------------------------------
|
||||
File: NJsonSchema
|
||||
-------------------------------------------------
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Rico Suter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
<?define FileArchitecture = "$(env.FileArchitecture)" ?>
|
||||
<Fragment>
|
||||
<DirectoryRef Id="$(var.ProductDirectoryName)">
|
||||
<Component Id="cmp3CC027D3F160412C9F0044EBED3115DD" Guid="{8D7CAA67-8F28-422C-85FB-BDE04902E64F}">
|
||||
<File Id="filFD2EF6BC74AF459D1BB52CA1E8C6E33B" KeyPath="yes" Source="$(env.ProductSourcePath)\NJsonSchema.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp3B130879A26D2E954251BB81E8948069" Guid="{CD268615-4603-4A5F-B126-340ADF2EDDD5}">
|
||||
<File Id="filAACDEEE28FEA076C73D082A0AD21B8E0" KeyPath="yes" Source="$(env.ProductSourcePath)\Microsoft.CSharp.dll" />
|
||||
</Component>
|
||||
|
@ -1818,6 +1821,7 @@
|
|||
</Fragment>
|
||||
<Fragment>
|
||||
<ComponentGroup Id="$(var.ProductDirectoryName)">
|
||||
<ComponentRef Id="cmp3CC027D3F160412C9F0044EBED3115DD" />
|
||||
<ComponentRef Id="cmp3B130879A26D2E954251BB81E8948069" />
|
||||
<ComponentRef Id="cmp02ABBE4A3EDBEBFD05DC17A009A2B79D" />
|
||||
<ComponentRef Id="cmpAA10498DF244C013CB5043C62E3AA83A" />
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.7.0" />
|
||||
<PackageReference Include="NJsonSchema" Version="9.9.10" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Management.Automation;
|
||||
using System.Management.Automation.Internal;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NJsonSchema;
|
||||
|
||||
namespace Microsoft.PowerShell.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// This class implements Test-Json command.
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsDiagnostic.Test, "Json", HelpUri = "")]
|
||||
public class TestJsonCommand : PSCmdlet
|
||||
{
|
||||
/// <summary>
|
||||
/// An JSON to be validated.
|
||||
/// </summary>
|
||||
[Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)]
|
||||
public String Json { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A 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.
|
||||
/// </summary>
|
||||
[Parameter(Position = 1)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
public String Schema { get; set; }
|
||||
|
||||
private JsonSchema4 _jschema;
|
||||
|
||||
/// <summary>
|
||||
/// Prepare an JSON schema.
|
||||
/// </summary>
|
||||
protected override void BeginProcessing()
|
||||
{
|
||||
if (Schema != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_jschema = JsonSchema4.FromJsonAsync(Schema).Result;
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
Exception exception = new Exception(TestJsonCmdletStrings.InvalidJsonSchema, exc);
|
||||
ThrowTerminatingError(new ErrorRecord(exception, "InvalidJsonSchema", ErrorCategory.InvalidData, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate an JSON.
|
||||
/// </summary>
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
JObject parsedJson = null;
|
||||
bool result = true;
|
||||
|
||||
try
|
||||
{
|
||||
parsedJson = JObject.Parse(Json);
|
||||
|
||||
if (_jschema != null)
|
||||
{
|
||||
var errorMessages = _jschema.Validate(parsedJson);
|
||||
if (errorMessages != null && errorMessages.Count != 0)
|
||||
{
|
||||
result = false;
|
||||
|
||||
Exception exception = new Exception(TestJsonCmdletStrings.InvalidJsonAgainstSchema);
|
||||
|
||||
foreach (var message in errorMessages)
|
||||
{
|
||||
ErrorRecord errorRecord = new ErrorRecord(exception, "InvalidJsonAgainstSchema", ErrorCategory.InvalidData, null);
|
||||
errorRecord.ErrorDetails = new ErrorDetails(message.ToString());
|
||||
WriteError(errorRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
result = false;
|
||||
|
||||
Exception exception = new Exception(TestJsonCmdletStrings.InvalidJson, exc);
|
||||
WriteError(new ErrorRecord(exception, "InvalidJson", ErrorCategory.InvalidData, Json));
|
||||
}
|
||||
|
||||
WriteObject(result);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="https://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="https://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="InvalidJsonSchema" xml:space="preserve">
|
||||
<value>Cannot parse the JSON schema.</value>
|
||||
</data>
|
||||
<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>
|
||||
</root>
|
|
@ -22,7 +22,8 @@ CmdletsToExport= "Format-List", "Format-Custom", "Format-Table", "Format-Wide",
|
|||
"Get-PSBreakpoint", "Remove-PSBreakpoint", "Enable-PSBreakpoint", "Disable-PSBreakpoint", "Get-PSCallStack",
|
||||
"Send-MailMessage", "Get-TraceSource", "Set-TraceSource", "Trace-Command", "Get-FileHash",
|
||||
"Get-Runspace", "Debug-Runspace", "Enable-RunspaceDebug", "Disable-RunspaceDebug",
|
||||
"Get-RunspaceDebug", "Wait-Debugger" , "Get-Uptime", "New-TemporaryFile", "Get-Verb", "Format-Hex", "Remove-Alias"
|
||||
"Get-RunspaceDebug", "Wait-Debugger" , "Get-Uptime", "New-TemporaryFile", "Get-Verb", "Format-Hex",
|
||||
"Test-Json", "Remove-Alias"
|
||||
FunctionsToExport= "Import-PowerShellDataFile"
|
||||
AliasesToExport= "fhx"
|
||||
NestedModules="Microsoft.PowerShell.Commands.Utility.dll","Microsoft.PowerShell.Utility.psm1"
|
||||
|
|
|
@ -22,7 +22,8 @@ CmdletsToExport= "Format-List", "Format-Custom", "Format-Table", "Format-Wide",
|
|||
"Get-PSBreakpoint", "Remove-PSBreakpoint", "New-TemporaryFile", "Enable-PSBreakpoint", "Disable-PSBreakpoint", "Get-PSCallStack",
|
||||
"Send-MailMessage", "Get-TraceSource", "Set-TraceSource", "Trace-Command", "Get-FileHash",
|
||||
"Unblock-File", "Get-Runspace", "Debug-Runspace", "Enable-RunspaceDebug", "Disable-RunspaceDebug",
|
||||
"Get-RunspaceDebug", "Wait-Debugger" , "Get-Uptime", "Get-Verb", "Format-Hex", "Remove-Alias"
|
||||
"Get-RunspaceDebug", "Wait-Debugger" , "Get-Uptime", "Get-Verb", "Format-Hex",
|
||||
"Test-Json", "Remove-Alias"
|
||||
FunctionsToExport= "ConvertFrom-SddlString"
|
||||
AliasesToExport= "fhx"
|
||||
NestedModules="Microsoft.PowerShell.Commands.Utility.dll","Microsoft.PowerShell.Utility.psm1"
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
Describe "Test-Json" -Tags "CI" {
|
||||
BeforeAll {
|
||||
$validSchemaJson = @"
|
||||
{
|
||||
'description': 'A person',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': {'type': 'string'},
|
||||
'hobbies': {
|
||||
'type': 'array',
|
||||
'items': {'type': 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
$invalidSchemaJson = @"
|
||||
{
|
||||
'description',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': {'type': 'string'},
|
||||
'hobbies': {
|
||||
'type': 'array',
|
||||
'items': {'type': 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
$validJson = @"
|
||||
{
|
||||
'name': 'James',
|
||||
'hobbies': ['.NET', 'Blogging', 'Reading', 'Xbox', 'LOLCATS']
|
||||
}
|
||||
"@
|
||||
|
||||
$invalidTypeInJson = @"
|
||||
{
|
||||
'name': 123,
|
||||
'hobbies': ['.NET', 'Blogging', 'Reading', 'Xbox', 'LOLCATS']
|
||||
}
|
||||
"@
|
||||
|
||||
$invalidTypeInJson2 = @"
|
||||
{
|
||||
'name': 123,
|
||||
'hobbies': [456, 'Blogging', 'Reading', 'Xbox', 'LOLCATS']
|
||||
}
|
||||
"@
|
||||
|
||||
$invalidNodeInJson = @"
|
||||
{
|
||||
'name': 'James',
|
||||
'hobbies': ['.NET', 'Blogging', 'Reading', 'Xbox', 'LOLCATS']
|
||||
errorNode
|
||||
}
|
||||
"@
|
||||
}
|
||||
|
||||
It "Json is valid" {
|
||||
Test-Json -Json $validJson | Should -BeTrue
|
||||
}
|
||||
|
||||
It "Json is valid aganist a valid schema" {
|
||||
Test-Json -Json $validJson -Schema $validSchemaJson | Should -BeTrue
|
||||
}
|
||||
|
||||
It "Json is invalid" {
|
||||
Test-Json -Json $invalidNodeInJson -ErrorAction SilentlyContinue | Should -BeFalse
|
||||
}
|
||||
|
||||
It "Json is invalid aganist a valid schema" {
|
||||
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" {
|
||||
{ Test-Json -Json $validJson -Schema $invalidSchemaJson -ErrorAction Stop } | ShouldBeErrorId "InvalidJsonSchema,Microsoft.PowerShell.Commands.TestJsonCommand"
|
||||
}
|
||||
|
||||
It "Test-Json write an error on invalid (<name>) Json aganist a valid schema" -TestCases @(
|
||||
@{ name = "type"; json = $invalidTypeInJson; error = "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" }
|
||||
@{ name = "node"; json = $invalidNodeInJson; error = "InvalidJson,Microsoft.PowerShell.Commands.TestJsonCommand" }
|
||||
) {
|
||||
param($json, $error)
|
||||
|
||||
$errorVar = $null
|
||||
Test-Json -Json $json -Schema $validSchemaJson -ErrorVariable errorVar -ErrorAction SilentlyContinue
|
||||
|
||||
$errorVar.FullyQualifiedErrorId | Should -BeExactly $error
|
||||
}
|
||||
|
||||
It "Test-Json return all errors when check invalid Json aganist a valid schema" {
|
||||
$errorVar = $null
|
||||
Test-Json -Json $invalidTypeInJson2 -Schema $validSchemaJson -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"
|
||||
}
|
||||
}
|
|
@ -440,6 +440,7 @@ Describe "Verify approved aliases list" -Tags "CI" {
|
|||
"Cmdlet", "Test-ComputerSecureChannel", , $($FullCLR )
|
||||
"Cmdlet", "Test-FileCatalog", , $($FullCLR -or $CoreWindows )
|
||||
"Cmdlet", "Test-ModuleManifest", , $($FullCLR -or $CoreWindows -or $CoreUnix)
|
||||
"Cmdlet", "Test-Json", , $( $CoreWindows -or $CoreUnix)
|
||||
"Cmdlet", "Test-Path", , $($FullCLR -or $CoreWindows -or $CoreUnix)
|
||||
"Cmdlet", "Test-PSSessionConfigurationFile", , $($FullCLR -or $CoreWindows )
|
||||
"Cmdlet", "Test-WSMan", , $($FullCLR -or $CoreWindows )
|
||||
|
|
Loading…
Reference in a new issue