Skip to content

Commit

Permalink
Merge pull request #270 from gregsdennis/schema/unknown-keywords
Browse files Browse the repository at this point in the history
added a keyword impl for unrecognized keywords instead of otherdata
  • Loading branch information
gregsdennis committed May 26, 2022
2 parents 23726d7 + 8c49206 commit bd394a0
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 59 deletions.
60 changes: 30 additions & 30 deletions .github/workflows/dotnet-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<details><summary>Expand to see formatting issues</summary>' >> 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 '</details>' >> 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 '<details><summary>Expand to see formatting issues</summary>' >> 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 '</details>' >> 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:
Expand Down
46 changes: 46 additions & 0 deletions JsonSchema.Tests/UnrecognizedKeywordTests.cs
Original file line number Diff line number Diff line change
@@ -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<JsonSchema>(schemaText);

Assert.AreEqual(1, schema!.Keywords!.Count);
Assert.IsInstanceOf<UnrecognizedKeyword>(schema.Keywords.First());
}

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

var schema = JsonSerializer.Deserialize<JsonSchema>(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<JsonSchema>(schemaText);

var reText = JsonSerializer.Serialize(schema);

Assert.AreEqual(schemaText, reText);
}
}
46 changes: 20 additions & 26 deletions JsonSchema/JsonSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using Json.More;
using Json.Pointer;

#pragma warning disable CS0618

namespace Json.Schema;

/// <summary>
Expand All @@ -21,23 +23,28 @@ public class JsonSchema : IRefResolvable, IEquatable<JsonSchema>
/// <summary>
/// The empty schema <code>{}</code>. Functionally equivalent to <see cref="True"/>.
/// </summary>
public static readonly JsonSchema Empty = new JsonSchema(Enumerable.Empty<IJsonSchemaKeyword>(), null);
public static readonly JsonSchema Empty = new(Enumerable.Empty<IJsonSchemaKeyword>(), null);
/// <summary>
/// The <code>true</code> schema. Passes all instances.
/// </summary>
public static readonly JsonSchema True = new JsonSchema(true);
public static readonly JsonSchema True = new(true);
/// <summary>
/// The <code>false</code> schema. Fails all instances.
/// </summary>
public static readonly JsonSchema False = new JsonSchema(false);
public static readonly JsonSchema False = new(false);

/// <summary>
/// Gets the keywords contained in the schema. Only populated for non-boolean schemas.
/// </summary>
public IReadOnlyCollection<IJsonSchemaKeyword>? Keywords { get; }
/// <summary>
/// Gets other non-keyword (or unknown keyword) properties in the schema.
/// (obsolete) Gets other non-keyword (or unknown keyword) properties in the schema.
/// </summary>
/// <remarks>
/// This property is now obsolete and no longer used. It will be removed at the next major version.
/// Until then, it will remain populated.
/// </remarks>
[Obsolete("Unrecognized keyword data now appears as UnrecognizedKeyword instances in the Keywords collection.")]
public IReadOnlyDictionary<string, JsonElement>? OtherData { get; }

/// <summary>
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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();
}
}
Expand Down
4 changes: 2 additions & 2 deletions JsonSchema/JsonSchema.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,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>2.3.0</Version>
<FileVersion>2.3.0.0</FileVersion>
<Version>2.4.0</Version>
<FileVersion>2.4.0.0</FileVersion>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<AssemblyName>JsonSchema.Net</AssemblyName>
Expand Down
26 changes: 26 additions & 0 deletions JsonSchema/JsonSchemaBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,32 @@ public static JsonSchemaBuilder UniqueItems(this JsonSchemaBuilder builder, bool
return builder;
}

/// <summary>
/// Adds a keyword that's not recognized by any vocabulary - extra data - to the schema.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="name">The keyword name.</param>
/// <param name="value">The value.</param>
/// <returns>The builder.</returns>
public static JsonSchemaBuilder Unrecognized(this JsonSchemaBuilder builder, string name, JsonElement value)
{
builder.Add(new UnrecognizedKeyword(name, value));
return builder;
}

/// <summary>
/// Adds a keyword that's not recognized by any vocabulary - extra data - to the schema.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="name">The keyword name.</param>
/// <param name="value">The value.</param>
/// <returns>The builder.</returns>
public static JsonSchemaBuilder Unrecognized(this JsonSchemaBuilder builder, string name, JsonElementProxy value)
{
builder.Add(new UnrecognizedKeyword(name, value));
return builder;
}

/// <summary>
/// Add an `$vocabulary` keyword.
/// </summary>
Expand Down
5 changes: 4 additions & 1 deletion JsonSchema/KeywordExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SchemaKeywordAttribute>().Name);

/// <summary>
Expand All @@ -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))
{
Expand Down
92 changes: 92 additions & 0 deletions JsonSchema/UnrecognizedKeyword.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Json.More;

namespace Json.Schema;

/// <summary>
/// Handles unrecognized keywords.
/// </summary>
[SchemaDraft(Draft.Draft6)]
[SchemaDraft(Draft.Draft7)]
[SchemaDraft(Draft.Draft201909)]
[SchemaDraft(Draft.Draft202012)]
[JsonConverter(typeof(UnrecognizedKeywordJsonConverter))]
public class UnrecognizedKeyword : IJsonSchemaKeyword, IEquatable<UnrecognizedKeyword>
{
/// <summary>
/// The name or key of the keyword.
/// </summary>
public string Name { get; }

/// <summary>
/// The value of the keyword.
/// </summary>
public JsonElement Value { get; }

/// <summary>
/// Creates a new <see cref="UnrecognizedKeyword"/>.
/// </summary>
/// <param name="name">The name of the keyword.</param>
/// <param name="value">Whether items should be unique.</param>
public UnrecognizedKeyword(string name, JsonElement value)
{
Name = name;
Value = value.Clone();
}

/// <summary>
/// Provides validation for the keyword.
/// </summary>
/// <param name="context">Contextual details for the validation process.</param>
public void Validate(ValidationContext context)
{
context.EnterKeyword(Name);
context.LocalResult.SetAnnotation(Name, Value);
context.LocalResult.Pass();
context.ExitKeyword(Name, context.LocalResult.IsValid);
}

/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
/// <param name="other">An object to compare with this object.</param>
/// <returns>true if the current object is equal to the <paramref name="other">other</paramref> parameter; otherwise, false.</returns>
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);
}

/// <summary>Determines whether the specified object is equal to the current object.</summary>
/// <param name="obj">The object to compare with the current object.</param>
/// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
public override bool Equals(object obj)
{
return Equals(obj as UnrecognizedKeyword);
}

/// <summary>Serves as the default hash function.</summary>
/// <returns>A hash code for the current object.</returns>
public override int GetHashCode()
{
unchecked
{
return (Name.GetHashCode() * 397) ^ Value.GetHashCode();
}
}
}

internal class UnrecognizedKeywordJsonConverter : JsonConverter<UnrecognizedKeyword>
{
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);
}
}
4 changes: 4 additions & 0 deletions json-everything.net/wwwroot/md/release-notes/json-schema.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down

0 comments on commit bd394a0

Please sign in to comment.