diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml index 9076ebf42f..5082938a23 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/dotnet-core.yml @@ -13,36 +13,36 @@ on: workflow_dispatch: jobs: - check-format: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Setup .NET Core - uses: actions/setup-dotnet@v2 - with: - # See https://stackoverflow.com/a/71619953/878701 - dotnet-version: 6.0.100 - - id: dotnet-format - run: | - dotnet format --verify-no-changes --report bin || true - FORMAT_REPORT=$(cat bin/format-report.json) - if [ "${FORMAT_REPORT}" != "[]" ]; then - echo '
Expand to see formatting issues' >> pr-message.md - echo >> pr-message.md - echo '```json' >> pr-message.md - cat bin/format-report.json >> pr-message.md - echo >> pr-message.md - echo '```' >> pr-message.md - echo >> pr-message.md - echo '
' >> pr-message.md - echo "::set-output name=POST_REPORT::true" - fi - - if: ${{ steps.dotnet-format.outputs.POST_REPORT == 'true' }} - uses: machine-learning-apps/pr-comment@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - path: pr-message.md + # check-format: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + # - name: Setup .NET Core + # uses: actions/setup-dotnet@v2 + # with: + # # See https://stackoverflow.com/a/71619953/878701 + # dotnet-version: 6.0.100 + # - id: dotnet-format + # run: | + # dotnet format --verify-no-changes --report bin || true + # FORMAT_REPORT=$(cat bin/format-report.json) + # if [ "${FORMAT_REPORT}" != "[]" ]; then + # echo '
Expand to see formatting issues' >> pr-message.md + # echo >> pr-message.md + # echo '```json' >> pr-message.md + # cat bin/format-report.json >> pr-message.md + # echo >> pr-message.md + # echo '```' >> pr-message.md + # echo >> pr-message.md + # echo '
' >> pr-message.md + # echo "::set-output name=POST_REPORT::true" + # fi + # - if: ${{ steps.dotnet-format.outputs.POST_REPORT == 'true' }} + # uses: machine-learning-apps/pr-comment@master + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # with: + # path: pr-message.md build: runs-on: ubuntu-latest steps: diff --git a/JsonSchema.Tests/UnrecognizedKeywordTests.cs b/JsonSchema.Tests/UnrecognizedKeywordTests.cs new file mode 100644 index 0000000000..a108a6fc72 --- /dev/null +++ b/JsonSchema.Tests/UnrecognizedKeywordTests.cs @@ -0,0 +1,46 @@ +using System.Linq; +using System.Text.Json; +using Json.More; +using NUnit.Framework; + +namespace Json.Schema.Tests; + +public class UnrecognizedKeywordTests +{ + [Test] + public void FooIsNotAKeyword() + { + var schemaText = "{\"foo\": \"bar\"}"; + + var schema = JsonSerializer.Deserialize(schemaText); + + Assert.AreEqual(1, schema!.Keywords!.Count); + Assert.IsInstanceOf(schema.Keywords.First()); + } + + [Test] + public void FooProducesAnAnnotation() + { + var schemaText = "{\"foo\": \"bar\"}"; + + var schema = JsonSerializer.Deserialize(schemaText); + + var result = schema!.Validate("{}", new ValidationOptions { OutputFormat = OutputFormat.Detailed }); + + Assert.IsTrue(result.IsValid); + Assert.AreEqual(1, result.Annotations.Count()); + Assert.IsTrue("bar".AsJsonElement().IsEquivalentTo((JsonElement)result.Annotations.First().Value)); + } + + [Test] + public void FooIsIncludedInSerialization() + { + var schemaText = "{\"foo\":\"bar\"}"; + + var schema = JsonSerializer.Deserialize(schemaText); + + var reText = JsonSerializer.Serialize(schema); + + Assert.AreEqual(schemaText, reText); + } +} \ No newline at end of file diff --git a/JsonSchema/JsonSchema.cs b/JsonSchema/JsonSchema.cs index fa4a9d6508..60e405bca4 100644 --- a/JsonSchema/JsonSchema.cs +++ b/JsonSchema/JsonSchema.cs @@ -9,6 +9,8 @@ using Json.More; using Json.Pointer; +#pragma warning disable CS0618 + namespace Json.Schema; /// @@ -21,23 +23,28 @@ public class JsonSchema : IRefResolvable, IEquatable /// /// The empty schema {}. Functionally equivalent to . /// - public static readonly JsonSchema Empty = new JsonSchema(Enumerable.Empty(), null); + public static readonly JsonSchema Empty = new(Enumerable.Empty(), null); /// /// The true schema. Passes all instances. /// - public static readonly JsonSchema True = new JsonSchema(true); + public static readonly JsonSchema True = new(true); /// /// The false schema. Fails all instances. /// - public static readonly JsonSchema False = new JsonSchema(false); + public static readonly JsonSchema False = new(false); /// /// Gets the keywords contained in the schema. Only populated for non-boolean schemas. /// public IReadOnlyCollection? Keywords { get; } /// - /// Gets other non-keyword (or unknown keyword) properties in the schema. + /// (obsolete) Gets other non-keyword (or unknown keyword) properties in the schema. /// + /// + /// This property is now obsolete and no longer used. It will be removed at the next major version. + /// Until then, it will remain populated. + /// + [Obsolete("Unrecognized keyword data now appears as UnrecognizedKeyword instances in the Keywords collection.")] public IReadOnlyDictionary? OtherData { get; } /// @@ -293,22 +300,15 @@ public void ValidateSubschema(ValidationContext context) break; } - if (newResolvable == null) + if (newResolvable is UnrecognizedKeyword unrecognized) { - // TODO: document that this process does not consider `$id` in extraneous data - if (resolvable is JsonSchema { OtherData: { } } subSchema && - subSchema.OtherData.TryGetValue(segment.Value, out var element)) - { - var newPointer = JsonPointer.Create(pointer.Segments.Skip(i + 1), true); - var value = newPointer.Evaluate(element); - var asSchema = FromText(value.ToString()); - return (asSchema, currentUri); - } - - return (null, currentUri); + var newPointer = JsonPointer.Create(pointer.Segments.Skip(i + 1), true); + var value = newPointer.Evaluate(unrecognized.Value); + var asSchema = FromText(value.ToString()); + return (asSchema, currentUri); } - resolvable = newResolvable; + resolvable = newResolvable!; } return (resolvable as JsonSchema, currentUri); @@ -431,6 +431,9 @@ public override JsonSchema Read(ref Utf8JsonReader reader, Type typeToConvert, J using var document = JsonDocument.ParseValue(ref reader); var element = document.RootElement; otherData[keyword] = element.Clone(); + + var unrecognizedKeyword = new UnrecognizedKeyword(keyword, element); + keywords.Add(unrecognizedKeyword); break; } @@ -473,15 +476,6 @@ public override void Write(Utf8JsonWriter writer, JsonSchema value, JsonSerializ JsonSerializer.Serialize(writer, keyword, keyword.GetType(), options); } - if (value.OtherData != null) - { - foreach (var data in value.OtherData) - { - writer.WritePropertyName(data.Key); - JsonSerializer.Serialize(writer, data.Value, options); - } - } - writer.WriteEndObject(); } } diff --git a/JsonSchema/JsonSchema.csproj b/JsonSchema/JsonSchema.csproj index e30ae1e3f4..0e3ee3b110 100644 --- a/JsonSchema/JsonSchema.csproj +++ b/JsonSchema/JsonSchema.csproj @@ -12,8 +12,8 @@ https://github.com/gregsdennis/json-everything https://github.com/gregsdennis/json-everything json-schema validation schema json - 2.3.0 - 2.3.0.0 + 2.4.0 + 2.4.0.0 2.0.0.0 LICENSE JsonSchema.Net diff --git a/JsonSchema/JsonSchemaBuilderExtensions.cs b/JsonSchema/JsonSchemaBuilderExtensions.cs index 701918d39c..e00a6ea83c 100644 --- a/JsonSchema/JsonSchemaBuilderExtensions.cs +++ b/JsonSchema/JsonSchemaBuilderExtensions.cs @@ -1060,6 +1060,32 @@ public static JsonSchemaBuilder UniqueItems(this JsonSchemaBuilder builder, bool return builder; } + /// + /// Adds a keyword that's not recognized by any vocabulary - extra data - to the schema. + /// + /// The builder. + /// The keyword name. + /// The value. + /// The builder. + public static JsonSchemaBuilder Unrecognized(this JsonSchemaBuilder builder, string name, JsonElement value) + { + builder.Add(new UnrecognizedKeyword(name, value)); + return builder; + } + + /// + /// Adds a keyword that's not recognized by any vocabulary - extra data - to the schema. + /// + /// The builder. + /// The keyword name. + /// The value. + /// The builder. + public static JsonSchemaBuilder Unrecognized(this JsonSchemaBuilder builder, string name, JsonElementProxy value) + { + builder.Add(new UnrecognizedKeyword(name, value)); + return builder; + } + /// /// Add an `$vocabulary` keyword. /// diff --git a/JsonSchema/KeywordExtensions.cs b/JsonSchema/KeywordExtensions.cs index b374a66ba3..624128f8e5 100644 --- a/JsonSchema/KeywordExtensions.cs +++ b/JsonSchema/KeywordExtensions.cs @@ -15,7 +15,8 @@ public static class KeywordExtensions .GetTypes() .Where(t => typeof(IJsonSchemaKeyword).IsAssignableFrom(t) && !t.IsAbstract && - !t.IsInterface) + !t.IsInterface && + t != typeof(UnrecognizedKeyword)) .ToDictionary(t => t, t => t.GetCustomAttribute().Name); /// @@ -29,6 +30,8 @@ public static string Keyword(this IJsonSchemaKeyword keyword) { if (keyword == null) throw new ArgumentNullException(nameof(keyword)); + if (keyword is UnrecognizedKeyword unrecognized) return unrecognized.Name; + var keywordType = keyword.GetType(); if (!_attributes.TryGetValue(keywordType, out var name)) { diff --git a/JsonSchema/UnrecognizedKeyword.cs b/JsonSchema/UnrecognizedKeyword.cs new file mode 100644 index 0000000000..ca159613a8 --- /dev/null +++ b/JsonSchema/UnrecognizedKeyword.cs @@ -0,0 +1,92 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Json.More; + +namespace Json.Schema; + +/// +/// Handles unrecognized keywords. +/// +[SchemaDraft(Draft.Draft6)] +[SchemaDraft(Draft.Draft7)] +[SchemaDraft(Draft.Draft201909)] +[SchemaDraft(Draft.Draft202012)] +[JsonConverter(typeof(UnrecognizedKeywordJsonConverter))] +public class UnrecognizedKeyword : IJsonSchemaKeyword, IEquatable +{ + /// + /// The name or key of the keyword. + /// + public string Name { get; } + + /// + /// The value of the keyword. + /// + public JsonElement Value { get; } + + /// + /// Creates a new . + /// + /// The name of the keyword. + /// Whether items should be unique. + public UnrecognizedKeyword(string name, JsonElement value) + { + Name = name; + Value = value.Clone(); + } + + /// + /// Provides validation for the keyword. + /// + /// Contextual details for the validation process. + public void Validate(ValidationContext context) + { + context.EnterKeyword(Name); + context.LocalResult.SetAnnotation(Name, Value); + context.LocalResult.Pass(); + context.ExitKeyword(Name, context.LocalResult.IsValid); + } + + /// Indicates whether the current object is equal to another object of the same type. + /// An object to compare with this object. + /// true if the current object is equal to the other parameter; otherwise, false. + public bool Equals(UnrecognizedKeyword? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Name == other.Name && Value.IsEquivalentTo(other.Value); + } + + /// Determines whether the specified object is equal to the current object. + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + public override bool Equals(object obj) + { + return Equals(obj as UnrecognizedKeyword); + } + + /// Serves as the default hash function. + /// A hash code for the current object. + public override int GetHashCode() + { + unchecked + { + return (Name.GetHashCode() * 397) ^ Value.GetHashCode(); + } + } +} + +internal class UnrecognizedKeywordJsonConverter : JsonConverter +{ + public override UnrecognizedKeyword Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException("Unrecognized keywords should be handled manually during JsonSchema deserialization."); + } + + public override void Write(Utf8JsonWriter writer, UnrecognizedKeyword value, JsonSerializerOptions options) + { + writer.WritePropertyName(value.Name); + writer.WriteValue(value.Value); + } +} \ No newline at end of file diff --git a/json-everything.net/wwwroot/md/release-notes/json-schema.md b/json-everything.net/wwwroot/md/release-notes/json-schema.md index 2ac4a438e1..e0cedd806e 100644 --- a/json-everything.net/wwwroot/md/release-notes/json-schema.md +++ b/json-everything.net/wwwroot/md/release-notes/json-schema.md @@ -1,3 +1,7 @@ +# [2.4.0](https://github.com/gregsdennis/json-everything/pull/270) + +Added `UnrecognizedKeyword` to represent keywords that were not recognized by any known vocabulary. The values of these keywords are then captured in the validation results as annotations. As a result of this change `JsonSchema.OtherData` has been marked obsolete. + # [2.3.0](https://github.com/gregsdennis/json-everything/pull/249) [#190](https://github.com/gregsdennis/json-everything/issues/190) - Added support for custom and localized error messages.