Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate Json schema using JsonSchema.NET #1626

Merged
merged 32 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
92dc278
Use JsonSchema.NET's Evaluate() method to validate a Json schema inst…
MaggieKimani1 Apr 15, 2024
b90aa03
Remove unnecessary using
MaggieKimani1 Apr 15, 2024
681f60a
Add a null check
MaggieKimani1 Apr 15, 2024
e8d504f
Make method public to expose it to clients
MaggieKimani1 Apr 16, 2024
1ecc6fd
Remove whitespace
MaggieKimani1 Apr 16, 2024
1d6b0a6
Decorate the nullable keyword with SchemaSpecVersion attribute for ev…
MaggieKimani1 Apr 16, 2024
f770dd6
Update assertions with correct test output post validation
MaggieKimani1 Apr 17, 2024
6ac327f
Fix failing test
MaggieKimani1 Apr 22, 2024
3f058f4
Refactor code to create JSON schema mappings without a Ref in the com…
MaggieKimani1 Apr 22, 2024
bd5287e
Merge remote-tracking branch 'origin/release/2.0.0' into mk/validate-…
MaggieKimani1 Apr 22, 2024
6e7c5a8
Fix failing tests
MaggieKimani1 Apr 22, 2024
e76101a
Adds an optional host document param to use during validation in orde…
MaggieKimani1 Apr 23, 2024
c829276
Walk each schema to resolve any present $refs
MaggieKimani1 Apr 26, 2024
2baf2f4
Code cleanup
MaggieKimani1 Apr 26, 2024
4591988
Update API interface
MaggieKimani1 Apr 26, 2024
3cae500
Adds an optional host document parameter
MaggieKimani1 Apr 29, 2024
7f5d24c
Register the host document with the schema registry for reference res…
MaggieKimani1 Apr 29, 2024
ff38301
Implement FindSubschema by fetching the referenced schema from our co…
MaggieKimani1 Apr 29, 2024
dcc7b7f
Fix failing tests
MaggieKimani1 Apr 29, 2024
babc887
Set the new schema's baseUri to match the document's
MaggieKimani1 Apr 29, 2024
d6593ab
Refactor V2 schema deserializer to update the referenceable schema's …
MaggieKimani1 Apr 29, 2024
0fb689d
Register the OpenApi-based vocabs
MaggieKimani1 Apr 29, 2024
85a2b54
Add a null check
MaggieKimani1 Apr 29, 2024
b09eb62
Revert the V31 deserializer
MaggieKimani1 Apr 29, 2024
a278267
Use the extension method from JsonSchema.NET to get a dicriminator ob…
MaggieKimani1 Apr 30, 2024
bb2726c
Merge branch 'mk/validate-json-schema' of https://github.com/microsof…
MaggieKimani1 Apr 30, 2024
e93cd4c
Use System.Text to deserialize a node into a JSON schema
MaggieKimani1 Apr 30, 2024
d1cb002
Replace Enumerable methods with indexing
MaggieKimani1 May 2, 2024
5c96d1c
Exclude files from build
MaggieKimani1 May 2, 2024
71c3963
Revert code to fix failing tests
MaggieKimani1 May 2, 2024
d0380ce
Remove static field modifier
MaggieKimani1 May 2, 2024
6cd0f02
Merge branch 'release/2.0.0' into mk/validate-json-schema
MaggieKimani1 May 2, 2024
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
10 changes: 8 additions & 2 deletions src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System;
Expand Down Expand Up @@ -204,7 +204,13 @@ public static Type MapJsonSchemaValueTypeToSimpleType(this JsonSchema schema)
return result;
}

internal static string ConvertSchemaValueTypeToString(SchemaValueType value)
/// <summary>
/// Converts the Schema value type to its string equivalent
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
public static string ConvertSchemaValueTypeToString(SchemaValueType value)
{
return value switch
{
Expand Down
1 change: 0 additions & 1 deletion src/Microsoft.OpenApi/Validations/Rules/JsonSchemaRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Linq;
using Json.Schema;
using Json.Schema.OpenApi;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Properties;

Expand Down
262 changes: 17 additions & 245 deletions src/Microsoft.OpenApi/Validations/Rules/RuleHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Json.Schema;
using Microsoft.OpenApi.Extensions;

namespace Microsoft.OpenApi.Validations.Rules
{
Expand Down Expand Up @@ -47,257 +45,31 @@
string ruleName,
JsonNode value,
JsonSchema schema)
{
if (schema == null)
{
return;
}

// Resolve the Json schema in memory before validating the data types.
var reference = schema.GetRef();
if (reference != null)
{
var referencePath = string.Concat("https://registry", reference.OriginalString.Split('#').Last());
var resolvedSchema = (JsonSchema)SchemaRegistry.Global.Get(new Uri(referencePath));
schema = resolvedSchema ?? schema;
}

var type = schema.GetJsonType()?.GetDisplayName();
var format = schema.GetFormat()?.Key;
var jsonElement = JsonSerializer.Deserialize<JsonElement>(value);

// Before checking the type, check first if the schema allows null.
// If so and the data given is also null, this is allowed for any type.
if (jsonElement.ValueKind is JsonValueKind.Null)
{
return;
}

if ("object".Equals(type, StringComparison.OrdinalIgnoreCase))
{
if (schema is not null)
{
// It is not against the spec to have a string representing an object value.
// To represent examples of media types that cannot naturally be represented in JSON or YAML,
// a string value can contain the example with escaping where necessary
if (jsonElement.ValueKind is JsonValueKind.String)
var results = schema.Evaluate(value, new EvaluationOptions()
{
return;
}
OutputFormat = OutputFormat.List
});

// If value is not a string and also not an object, there is a data mismatch.
if (jsonElement.ValueKind is not JsonValueKind.Object)
if (!results.IsValid)
{
context.CreateWarning(
ruleName,
DataTypeMismatchedErrorMessage);
return;
}

if (value is JsonObject anyObject)
{
foreach (var property in anyObject)
foreach (var detail in results.Details)
{
context.Enter(property.Key);
if ((schema.GetProperties()?.TryGetValue(property.Key, out var propertyValue)) ?? false)
{
ValidateDataTypeMismatch(context, ruleName, anyObject[property.Key], propertyValue);
}
else
if (detail.Errors != null && detail.Errors.Any())
{
ValidateDataTypeMismatch(context, ruleName, anyObject[property.Key], schema.GetAdditionalProperties());
foreach (var error in detail.Errors)
{
if (!string.IsNullOrEmpty(error.Key) || !string.IsNullOrEmpty(error.Value.Trim()))
{
context.CreateWarning(ruleName, string.Format("{0} : {1} at {2}", error.Key, error.Value.Trim(), detail.InstanceLocation));
}
}
Fixed Show fixed Hide fixed
}

context.Exit();
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
}

return;
}

if ("array".Equals(type, StringComparison.OrdinalIgnoreCase))
{
// It is not against the spec to have a string representing an array value.
// To represent examples of media types that cannot naturally be represented in JSON or YAML,
// a string value can contain the example with escaping where necessary
if (jsonElement.ValueKind is JsonValueKind.String)
{
return;
}

// If value is not a string and also not an array, there is a data mismatch.
if (value is not JsonArray)
{
context.CreateWarning(
ruleName,
DataTypeMismatchedErrorMessage);
return;
}

var anyArray = value as JsonArray;

for (int i = 0; i < anyArray.Count; i++)
{
context.Enter(i.ToString());

ValidateDataTypeMismatch(context, ruleName, anyArray[i], schema.GetItems());

context.Exit();
}

return;
}

if ("integer".Equals(type, StringComparison.OrdinalIgnoreCase) &&
"int32".Equals(format, StringComparison.OrdinalIgnoreCase))
{
if (jsonElement.ValueKind is not JsonValueKind.Number)
{
context.CreateWarning(
ruleName,
DataTypeMismatchedErrorMessage);
}

return;
}

if ("integer".Equals(type, StringComparison.OrdinalIgnoreCase) &&
"int64".Equals(format, StringComparison.OrdinalIgnoreCase))
{
if (jsonElement.ValueKind is not JsonValueKind.Number)
{
context.CreateWarning(
ruleName,
DataTypeMismatchedErrorMessage);
}

return;
}

if ("integer".Equals(type, StringComparison.OrdinalIgnoreCase) &&
jsonElement.ValueKind is not JsonValueKind.Number)
{
if (jsonElement.ValueKind is not JsonValueKind.Number)
{
context.CreateWarning(
ruleName,
DataTypeMismatchedErrorMessage);
}

return;
}

if ("number".Equals(type, StringComparison.OrdinalIgnoreCase) &&
"float".Equals(format, StringComparison.OrdinalIgnoreCase))
{
if (jsonElement.ValueKind is not JsonValueKind.Number)
{
context.CreateWarning(
ruleName,
DataTypeMismatchedErrorMessage);
}

return;
}

if ("number".Equals(type, StringComparison.OrdinalIgnoreCase) &&
"double".Equals(format, StringComparison.OrdinalIgnoreCase))
{
if (jsonElement.ValueKind is not JsonValueKind.Number)
{
context.CreateWarning(
ruleName,
DataTypeMismatchedErrorMessage);
}

return;
}

if ("number".Equals(type, StringComparison.OrdinalIgnoreCase))
{
if (jsonElement.ValueKind is not JsonValueKind.Number)
{
context.CreateWarning(
ruleName,
DataTypeMismatchedErrorMessage);
}

return;
}

if ("string".Equals(type, StringComparison.OrdinalIgnoreCase) &&
"byte".Equals(format, StringComparison.OrdinalIgnoreCase))
{
if (jsonElement.ValueKind is not JsonValueKind.String)
{
context.CreateWarning(
ruleName,
DataTypeMismatchedErrorMessage);
}

return;
}

if ("string".Equals(type, StringComparison.OrdinalIgnoreCase) &&
"date".Equals(format, StringComparison.OrdinalIgnoreCase))
{
if (jsonElement.ValueKind is not JsonValueKind.String)
{
context.CreateWarning(
ruleName,
DataTypeMismatchedErrorMessage);
}

return;
}

if ("string".Equals(type, StringComparison.OrdinalIgnoreCase) &&
"date-time".Equals(format, StringComparison.OrdinalIgnoreCase))
{
if (jsonElement.ValueKind is not JsonValueKind.String)
{
context.CreateWarning(
ruleName,
DataTypeMismatchedErrorMessage);
}

return;
}

if ("string".Equals(type, StringComparison.OrdinalIgnoreCase) &&
"password".Equals(format, StringComparison.OrdinalIgnoreCase))
{
if (jsonElement.ValueKind is not JsonValueKind.String)
{
context.CreateWarning(
ruleName,
DataTypeMismatchedErrorMessage);
}

return;
}

if ("string".Equals(type, StringComparison.OrdinalIgnoreCase))
{
if (jsonElement.ValueKind is not JsonValueKind.String)
{
context.CreateWarning(
ruleName,
DataTypeMismatchedErrorMessage);
}

return;
}

if ("boolean".Equals(type, StringComparison.OrdinalIgnoreCase))
{
if (jsonElement.ValueKind is not JsonValueKind.True and not JsonValueKind.False)
{
context.CreateWarning(
ruleName,
DataTypeMismatchedErrorMessage);
}

return;
}
}
}
}
}
Loading