Skip to content

Commit

Permalink
Merge pull request #547 from gregsdennis/schema/unknown-keywords-anno…
Browse files Browse the repository at this point in the history
…tation

Schema/unknown keywords annotation
  • Loading branch information
gregsdennis committed Oct 30, 2023
2 parents f818b8a + ea6815b commit 9771c71
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 9 deletions.
48 changes: 45 additions & 3 deletions JsonSchema.Tests/UnrecognizedKeywordTests.cs
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Encodings.Web;
using System.Text.Json;
Expand Down Expand Up @@ -32,7 +33,48 @@ public void FooProducesAnAnnotation()

Assert.IsTrue(result.IsValid);
Assert.AreEqual(1, result.Annotations!.Count);
Assert.IsTrue(((JsonNode?)"bar").IsEquivalentTo(result.Annotations.First().Value));
Assert.IsTrue(((JsonNode?)"bar").IsEquivalentTo(result.Annotations["foo"]));
}

[Test]
public void UnknownKeywordAnnotationIsProduced()
{
var schemaText = "{\"foo\": \"bar\"}";

var schema = JsonSerializer.Deserialize<JsonSchema>(schemaText);

var result = schema!.Evaluate(new JsonObject(), new EvaluationOptions
{
OutputFormat = OutputFormat.Hierarchical,
AddAnnotationForUnknownKeywords = true
});

Assert.IsTrue(result.IsValid);
Assert.AreEqual(2, result.Annotations!.Count);
Assert.IsTrue(new JsonArray{"foo"}.IsEquivalentTo(result.Annotations["$unknownKeywords"]));
}

[Test]
public void AnnotationsProducedForKnownButUnused()
{
var schema = new JsonSchemaBuilder()
.Schema(MetaSchemas.Draft202012Id)
.Dependencies(new Dictionary<string, SchemaOrPropertyList>
{
["foo"] = (JsonSchema)false
});

var instance = new JsonObject { ["bar"] = 5 };
var result = schema.Evaluate(instance, new EvaluationOptions
{
OutputFormat = OutputFormat.Hierarchical,
AddAnnotationForUnknownKeywords = true
});

Assert.IsTrue(result.IsValid);
Assert.AreEqual(2, result.Annotations!.Count);
Assert.IsTrue(new JsonObject { ["foo"] = false }.IsEquivalentTo(result.Annotations["dependencies"]));
Assert.IsTrue(new JsonArray{"dependencies"}.IsEquivalentTo(result.Annotations["$unknownKeywords"]));
}

[Test]
Expand All @@ -46,7 +88,7 @@ public void FooProducesAnAnnotation_Constructed()

Assert.IsTrue(result.IsValid);
Assert.AreEqual(1, result.Annotations!.Count);
Assert.IsTrue(((JsonNode?)"bar").IsEquivalentTo(result.Annotations.First().Value));
Assert.IsTrue(((JsonNode?)"bar").IsEquivalentTo(result.Annotations["foo"]));
}

[Test]
Expand Down Expand Up @@ -74,7 +116,7 @@ public void FooProducesAnAnnotation_WithSchemaKeyword()

Assert.IsTrue(result.IsValid);
Assert.AreEqual(1, result.Annotations!.Count);
Assert.IsTrue(((JsonNode?)"bar").IsEquivalentTo(result.Annotations.First().Value));
Assert.IsTrue(((JsonNode?)"bar").IsEquivalentTo(result.Annotations["foo"]));
}

[Test]
Expand Down
7 changes: 7 additions & 0 deletions JsonSchema/EvaluationOptions.cs
Expand Up @@ -85,6 +85,12 @@ public class EvaluationOptions
/// </summary>
public bool PreserveDroppedAnnotations { get; set; }

/// <summary>
/// Outputs an annotation that lists any unknown keywords. Can be
/// useful for catching typos.
/// </summary>
public bool AddAnnotationForUnknownKeywords { get; set; }

/// <summary>
/// Gets the set of keyword types from which annotations will be ignored.
/// </summary>
Expand Down Expand Up @@ -126,6 +132,7 @@ public static EvaluationOptions From(EvaluationOptions other)
ProcessCustomKeywords = other.ProcessCustomKeywords,
OnlyKnownFormats = other.OnlyKnownFormats,
PreserveDroppedAnnotations = other.PreserveDroppedAnnotations,
AddAnnotationForUnknownKeywords = other.AddAnnotationForUnknownKeywords,
Culture = other.Culture,
_ignoredAnnotationTypes = other._ignoredAnnotationTypes == null
? null
Expand Down
28 changes: 25 additions & 3 deletions JsonSchema/JsonSchema.cs
Expand Up @@ -19,6 +19,8 @@ namespace Json.Schema;
[DebuggerDisplay("{ToDebugString()}")]
public class JsonSchema : IBaseDocument
{
private const string _unknownKeywordsAnnotationKey = "$unknownKeywords";

private readonly Dictionary<string, IJsonSchemaKeyword>? _keywords;
private readonly List<(DynamicScope Scope, SchemaConstraint Constraint)> _constraints = new();

Expand Down Expand Up @@ -236,6 +238,8 @@ public EvaluationResults Evaluate(JsonNode? root, EvaluationOptions? options = n
var evaluation = constraint.BuildEvaluation(root, JsonPointer.Empty, JsonPointer.Empty, options);
evaluation.Evaluate(context);

if (options.AddAnnotationForUnknownKeywords && constraint.UnknownKeywords != null)
evaluation.Results.SetAnnotation(_unknownKeywordsAnnotationKey, constraint.UnknownKeywords);

var results = evaluation.Results;
switch (options.OutputFormat)
Expand Down Expand Up @@ -358,13 +362,31 @@ private void PopulateConstraint(SchemaConstraint constraint, EvaluationContext c
}
var localConstraints = new List<KeywordConstraint>();
var version = DeclaredVersion == SpecVersion.Unspecified ? context.EvaluatingAs : DeclaredVersion;
var keywords = EvaluationOptions.FilterKeywords(context.GetKeywordsToProcess(this, context.Options), version);
var keywords = EvaluationOptions.FilterKeywords(context.GetKeywordsToProcess(this, context.Options), version).ToArray();
var unrecognized = Keywords!.OfType<UnrecognizedKeyword>();
var unrecognizedButSupported = Keywords!.Except(keywords).ToArray();
if (context.Options.AddAnnotationForUnknownKeywords)
constraint.UnknownKeywords = new JsonArray(unrecognizedButSupported.Concat(unrecognized)
.Select(x => (JsonNode?)x.Keyword())
.ToArray());
foreach (var keyword in keywords.OrderBy(x => x.Priority()))
{
var keywordConstraint = keyword.GetConstraint(constraint, localConstraints, context);
localConstraints.Add(keywordConstraint);
}

foreach (var keyword in unrecognizedButSupported)
{
var serialized = JsonSerializer.Serialize((object) keyword);
// TODO: The current keyword serializations include the keyword property name.
// This is an oversight and needs to be fixed in future versions.
// This is a breaking change: users may have their own keywords.
var jsonText = serialized.Split(new[] { ':' }, 2)[1];
var json = JsonNode.Parse(jsonText);
var keywordConstraint = KeywordConstraint.SimpleAnnotation(keyword.Keyword(), json);
localConstraints.Add(keywordConstraint);
}

constraint.Constraints = localConstraints.ToArray();
if (dynamicScopeChanged)
{
Expand Down Expand Up @@ -582,7 +604,7 @@ internal static IEnumerable<JsonSchema> GetSubschemas(IJsonSchemaKeyword keyword
var serialized = JsonSerializer.Serialize(localResolvable);
// TODO: The current keyword serializations include the keyword property name.
// This is an oversight and needs to be fixed in future versions.
// This is a breaking change.
// This is a breaking change: users may have their own keywords.
var jsonText = serialized.Split(new[] { ':' }, 2)[1];
var json = JsonNode.Parse(jsonText);
var newPointer = JsonPointer.Create(pointer.Segments.Skip(i));
Expand Down Expand Up @@ -716,7 +738,7 @@ public override void Write(Utf8JsonWriter writer, JsonSchema value, JsonSerializ
writer.WriteStartObject();
foreach (var keyword in value.Keywords!)
{
// TODO: The property name should be written here, probably.
// TODO: The property name should be written here, probably. (breaking change: users may have their own keywords)
JsonSerializer.Serialize(writer, keyword, keyword.GetType(), options);
}

Expand Down
4 changes: 2 additions & 2 deletions JsonSchema/JsonSchema.csproj
Expand Up @@ -16,8 +16,8 @@
<PackageProjectUrl>https://github.com/gregsdennis/json-everything</PackageProjectUrl>
<RepositoryUrl>https://github.com/gregsdennis/json-everything</RepositoryUrl>
<PackageTags>json-schema validation schema json</PackageTags>
<Version>5.2.7</Version>
<FileVersion>5.2.7.0</FileVersion>
<Version>5.3.0</Version>
<FileVersion>5.3.0.0</FileVersion>
<AssemblyVersion>5.0.0.0</AssemblyVersion>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<AssemblyName>JsonSchema.Net</AssemblyName>
Expand Down
1 change: 1 addition & 0 deletions JsonSchema/SchemaConstraint.cs
Expand Up @@ -56,6 +56,7 @@ public class SchemaConstraint
internal JsonPointer BaseSchemaOffset { get; set; } = JsonPointer.Empty;
internal SchemaConstraint? Source { get; set; }
internal bool UseLocatorAsInstance { get; set; }
internal JsonArray? UnknownKeywords { get; set; }

internal SchemaConstraint(JsonPointer relativeEvaluationPath, JsonPointer baseInstanceLocation, JsonPointer relativeInstanceLocation, Uri schemaBaseUri, JsonSchema localSchema)
{
Expand Down
14 changes: 13 additions & 1 deletion json-everything.net/Pages/Schema.razor
Expand Up @@ -116,6 +116,16 @@
</span>
</span>
</label>
<label class="my-2">
<InputCheckbox @bind-Value="_options.AddAnnotationForUnknownKeywords" DisplayName="Unknown keywords annotation" />
Unknown keywords annotation
<span class="tooltip-icon">
<span class="tooltip-text">
<MarkdownSpan Content="@HelpContent.SchemaUnknownKeywordsAnnotation"></MarkdownSpan>
</span>
</span>
</label>
<label class="my-2">
<InputCheckbox @bind-Value="_options.ValidateFormat" DisplayName="Validate format"/>
Validate <span class="font-monospace">format</span>
Expand Down Expand Up @@ -212,13 +222,15 @@
public SpecVersion OutputStructure { get; set; } = SpecVersion.DraftNext;
public bool ValidateFormat { get; set; }
public bool IncludeDroppedAnnotations { get; set; }
public bool AddAnnotationForUnknownKeywords { get; set; }

public EvaluationOptions ToValidationOptions() => new()
{
OutputFormat = OutputFormat,
EvaluateAs = Version,
RequireFormatValidation = ValidateFormat,
PreserveDroppedAnnotations = IncludeDroppedAnnotations
PreserveDroppedAnnotations = IncludeDroppedAnnotations,
AddAnnotationForUnknownKeywords = AddAnnotationForUnknownKeywords
};
}

Expand Down
6 changes: 6 additions & 0 deletions json-everything.net/Services/HelpContent.cs
Expand Up @@ -90,6 +90,12 @@ public static class HelpContent
This option will include these annotations under a `droppedAnnotations` property when using the
""Draft-Next"" output. This can be useful for debugging.";

public const string SchemaUnknownKeywordsAnnotation = @"
(experimental) Adds an annotation to the output that contains a list of schema keywords that were
unrecognized by its meta-schema. See
[this JSON Schema discussion](https://github.com/orgs/json-schema-org/discussions/512) for more
information to provide feedback on the potential feature.";

public const string SchemaValidateFormat = @"
Allows you to specify whether the `format` keyword should be asserted. Typically this is an
annotation-only keyword, meaning that it will appear in the output, but it will not be validated.";
Expand Down
4 changes: 4 additions & 0 deletions tools/ApiDocsGenerator/release-notes/rn-json-schema.md
Expand Up @@ -4,6 +4,10 @@ title: JsonSchema.Net
icon: fas fa-tag
order: "8.01"
---
# [5.3.0](https://github.com/gregsdennis/json-everything/pull/534) {#release-schema-5.3.0}

Added `EvaluationOptions.AddAnnotationForUnknownKeywords` which adds an annotation that contains an array of unknown keywords in the schema.

# [5.2.7](https://github.com/gregsdennis/json-everything/pull/534) {#release-schema-5.2.7}

Fixed an issue with resolving `$ref`s that point into supported non-applicator keywords. (JSON Schema optional behavior)
Expand Down

0 comments on commit 9771c71

Please sign in to comment.