Skip to content

Commit

Permalink
Merge pull request #344 from gregsdennis/issue340-custom-keyword-support
Browse files Browse the repository at this point in the history
add custom keyword without vocabs support for 2019-09 and later
  • Loading branch information
gregsdennis committed Nov 6, 2022
2 parents d696842 + 25e023b commit 37d3aac
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 35 deletions.
5 changes: 5 additions & 0 deletions JsonSchema.Tests/BuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ public static JsonSchemaBuilder MinDate(this JsonSchemaBuilder builder, DateTime
builder.Add(new VocabularyTests.MinDateKeyword(date));
return builder;
}
public static JsonSchemaBuilder NonVocabMinDate(this JsonSchemaBuilder builder, DateTime date)
{
builder.Add(new VocabularyTests.NonVocabMinDateKeyword(date));
return builder;
}
public static JsonSchemaBuilder MaxDate(this JsonSchemaBuilder builder, DateTime date)
{
builder.Add(new VocabularyTests.MaxDateKeyword(date));
Expand Down
50 changes: 32 additions & 18 deletions JsonSchema.Tests/FetchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,34 +85,48 @@ public void LocalRegistryMissesRef()
[Test]
public void GlobalRegistryMissesRef()
{
var options = new ValidationOptions
{
OutputFormat = OutputFormat.Detailed
};
SchemaRegistry.Global.Fetch = uri =>
try
{
if (uri.AbsoluteUri == "http://my.schema/test2")
return JsonSchema.FromText("{\"type\": \"string\"}");
return null;
};
var schema = JsonSchema.FromText("{\"$ref\":\"http://my.schema/test1\"}");
var options = new ValidationOptions
{
OutputFormat = OutputFormat.Detailed
};
SchemaRegistry.Global.Fetch = uri =>
{
if (uri.AbsoluteUri == "http://my.schema/test2")
return JsonSchema.FromText("{\"type\": \"string\"}");
return null;
};
var schema = JsonSchema.FromText("{\"$ref\":\"http://my.schema/test1\"}");

using var json = JsonDocument.Parse("10");
using var json = JsonDocument.Parse("10");

var results = schema.Validate(json.RootElement, options);
var results = schema.Validate(json.RootElement, options);

results.AssertInvalid();
results.SchemaLocation.Segments.Last().Value.Should().Be("$ref");
results.AssertInvalid();
results.SchemaLocation.Segments.Last().Value.Should().Be("$ref");
}
finally
{
SchemaRegistry.Global.Fetch = null!;
}
}

[Test]
public void RefContainsBadJson()
{
SchemaRegistry.Global.Fetch = _ => JsonSchema.FromText("{\"type\": \"string\", \"invalid\"}");
var schema = JsonSchema.FromText("{\"$ref\":\"http://my.schema/test1\"}");
try
{
SchemaRegistry.Global.Fetch = _ => JsonSchema.FromText("{\"type\": \"string\", \"invalid\"}");
var schema = JsonSchema.FromText("{\"$ref\":\"http://my.schema/test1\"}");

using var json = JsonDocument.Parse("10");
using var json = JsonDocument.Parse("10");

Assert.Throws<JsonException>(() => schema.Validate(json.RootElement));
Assert.Throws<JsonException>(() => schema.Validate(json.RootElement));
}
finally
{
SchemaRegistry.Global.Fetch = null!;
}
}
}
143 changes: 130 additions & 13 deletions JsonSchema.Tests/VocabularyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class VocabularyTests
[SchemaKeyword(Name)]
[SchemaDraft(Draft.Draft201909 | Draft.Draft202012)]
[JsonConverter(typeof(MinDateJsonConverter))]
[Vocabulary("http://mydates.com/vocabulary")]
public class MinDateKeyword : IJsonSchemaKeyword, IEquatable<MinDateKeyword>
{
internal const string Name = "minDate";
Expand Down Expand Up @@ -74,9 +75,74 @@ public override void Write(Utf8JsonWriter writer, MinDateKeyword value, JsonSeri
}
}

[SchemaKeyword(Name)]
[SchemaDraft(Draft.Draft7 | Draft.Draft201909 | Draft.Draft202012)]
[JsonConverter(typeof(NonVocabMinDateJsonConverter))]
public class NonVocabMinDateKeyword : IJsonSchemaKeyword, IEquatable<NonVocabMinDateKeyword>
{
internal const string Name = "minDate-nv";

public DateTime Date { get; }

public NonVocabMinDateKeyword(DateTime date)
{
Date = date;
}

public void Validate(ValidationContext context)
{
var dateString = context.LocalInstance!.GetValue<string>();
var date = DateTime.Parse(dateString);

if (date >= Date)
context.LocalResult.Pass();
else
context.LocalResult.Fail("[[provided:O]] must be on or after [[value:O]]",
("provided", date),
("value", Date));
}

public bool Equals(NonVocabMinDateKeyword? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Date.Equals(other.Date);
}

public override bool Equals(object? obj)
{
return Equals(obj as NonVocabMinDateKeyword);
}

public override int GetHashCode()
{
return Date.GetHashCode();
}
}

private class NonVocabMinDateJsonConverter : JsonConverter<NonVocabMinDateKeyword>
{
public override NonVocabMinDateKeyword Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
throw new JsonException("Expected string");

var dateString = reader.GetString();
var date = DateTime.Parse(dateString!, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
return new NonVocabMinDateKeyword(date);
}

public override void Write(Utf8JsonWriter writer, NonVocabMinDateKeyword value, JsonSerializerOptions options)
{
writer.WritePropertyName(NonVocabMinDateKeyword.Name);
writer.WriteStringValue(value.Date.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssK"));
}
}

[SchemaKeyword(Name)]
[SchemaDraft(Draft.Draft201909 | Draft.Draft202012)]
[JsonConverter(typeof(MaxDateJsonConverter))]
[Vocabulary("http://mydates.com/vocabulary")]
public class MaxDateKeyword : IJsonSchemaKeyword, IEquatable<MaxDateKeyword>
{
internal const string Name = "maxDate";
Expand Down Expand Up @@ -170,13 +236,15 @@ public override void Write(Utf8JsonWriter writer, MaxDateKeyword value, JsonSeri
public void Setup()
{
SchemaKeywordRegistry.Register<MinDateKeyword>();
SchemaKeywordRegistry.Register<NonVocabMinDateKeyword>();
SchemaKeywordRegistry.Register<MaxDateKeyword>();
}

[OneTimeTearDown]
public void TearDown()
{
SchemaKeywordRegistry.Unregister<MinDateKeyword>();
SchemaKeywordRegistry.Register<NonVocabMinDateKeyword>();
SchemaKeywordRegistry.Unregister<MaxDateKeyword>();
}

Expand All @@ -185,7 +253,7 @@ public void SchemaValidation_ValidateMetaSchemaTrue_VocabularyNotKnown()
{
JsonSchema schema = new JsonSchemaBuilder()
.Schema("http://mydates.com/schema")
.MinDate(DateTime.Now.AddDays(-1));
.MinDate(DateTime.Now.ToUniversalTime().AddDays(-1));
var instance = JsonNode.Parse($"\"{DateTime.Now.ToUniversalTime():O}\"");

var options = new ValidationOptions
Expand All @@ -199,17 +267,16 @@ public void SchemaValidation_ValidateMetaSchemaTrue_VocabularyNotKnown()
Console.WriteLine();
Console.WriteLine(instance);
Console.WriteLine();
Console.WriteLine(JsonSerializer.Serialize(results, _serializerOptions));

Assert.IsFalse(results.IsValid);
results.AssertInvalid();
}

[Test]
public void SchemaValidation_ValidateMetaSchemaFalse_VocabularyNotKnown()
{
JsonSchema schema = new JsonSchemaBuilder()
.Schema("http://mydates.com/schema")
.MinDate(DateTime.Now.AddDays(-1));
.MinDate(DateTime.Now.ToUniversalTime().AddDays(-1));
var instance = JsonNode.Parse($"\"{DateTime.Now.ToUniversalTime():O}\"");

var options = new ValidationOptions();
Expand All @@ -220,17 +287,70 @@ public void SchemaValidation_ValidateMetaSchemaFalse_VocabularyNotKnown()
Console.WriteLine();
Console.WriteLine(instance);
Console.WriteLine();
Console.WriteLine(JsonSerializer.Serialize(results, _serializerOptions));

Assert.IsTrue(results.IsValid);
results.AssertValid();
}

[Test]
public void SchemaValidation_ValidateMetaSchemaFalse_NonVocab_Draft201909_NoCustomKeywords()
{
JsonSchema schema = new JsonSchemaBuilder()
.Schema(MetaSchemas.Draft201909Id)
.NonVocabMinDate(DateTime.Now.ToUniversalTime().AddDays(1));
var instance = JsonNode.Parse($"\"{DateTime.Now.ToUniversalTime():O}\"");

var results = schema.Validate(instance);

Console.WriteLine(JsonSerializer.Serialize(schema, _serializerOptions));
Console.WriteLine();
Console.WriteLine(instance);
Console.WriteLine();

results.AssertValid();
}

[Test]
public void SchemaValidation_ValidateMetaSchemaFalse_NonVocab_Draft201909_WithCustomKeywords()
{
JsonSchema schema = new JsonSchemaBuilder()
.Schema(MetaSchemas.Draft201909Id)
.NonVocabMinDate(DateTime.Now.ToUniversalTime().AddDays(1));
var instance = JsonNode.Parse($"\"{DateTime.Now.ToUniversalTime():O}\"");

var results = schema.Validate(instance, new ValidationOptions{ProcessCustomKeywords = true});

Console.WriteLine(JsonSerializer.Serialize(schema, _serializerOptions));
Console.WriteLine();
Console.WriteLine(instance);
Console.WriteLine();

results.AssertInvalid();
}

[Test]
public void SchemaValidation_ValidateMetaSchemaFalse_NonVocab_Draft7()
{
JsonSchema schema = new JsonSchemaBuilder()
.Schema(MetaSchemas.Draft7Id)
.NonVocabMinDate(DateTime.Now.ToUniversalTime().AddDays(1));
var instance = JsonNode.Parse($"\"{DateTime.Now.ToUniversalTime():O}\"");

var results = schema.Validate(instance);

Console.WriteLine(JsonSerializer.Serialize(schema, _serializerOptions));
Console.WriteLine();
Console.WriteLine(instance);
Console.WriteLine();

results.AssertInvalid();
}

[Test]
public void SchemaValidation_ValidateMetaSchemaTrue_VocabularyKnown()
{
JsonSchema schema = new JsonSchemaBuilder()
.Schema("http://mydates.com/schema")
.MinDate(DateTime.Now.AddDays(-1));
.MinDate(DateTime.Now.ToUniversalTime().AddDays(-1));
var instance = JsonNode.Parse($"\"{DateTime.Now.ToUniversalTime():O}\"");

var options = new ValidationOptions
Expand All @@ -245,9 +365,8 @@ public void SchemaValidation_ValidateMetaSchemaTrue_VocabularyKnown()
Console.WriteLine();
Console.WriteLine(instance);
Console.WriteLine();
Console.WriteLine(JsonSerializer.Serialize(results, _serializerOptions));

Assert.IsTrue(results.IsValid);
results.AssertValid();
}

[Test]
Expand All @@ -262,9 +381,8 @@ public void MetaSchemaValidation_VocabularyNotKnown()

Console.WriteLine(schemaAsJson);
Console.WriteLine();
Console.WriteLine(JsonSerializer.Serialize(results, _serializerOptions));

Assert.IsFalse(results.IsValid);
results.AssertInvalid();
}

[Test]
Expand All @@ -281,8 +399,7 @@ public void MetaSchemaValidation_VocabularyKnown()

Console.WriteLine(schemaAsJson);
Console.WriteLine();
Console.WriteLine(JsonSerializer.Serialize(results, _serializerOptions));

Assert.IsTrue(results.IsValid);
results.AssertValid();
}
}
2 changes: 1 addition & 1 deletion JsonSchema/JsonSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public void ValidateSubschema(ValidationContext context)
{
// $schema is always processed first, and this should only be set
// after $schema has been evaluated.
if (keyword is not SchemaKeyword)
if (keyword is not SchemaKeyword && !context.Options.ProcessCustomKeywords)
keywordTypesToProcess ??= context.GetKeywordsToProcess()?.ToList();
if (!keywordTypesToProcess?.Contains(keyword.GetType()) ?? false) continue;

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>3.2.1</Version>
<FileVersion>3.2.1.0</FileVersion>
<Version>3.3.0</Version>
<FileVersion>3.3.0.0</FileVersion>
<AssemblyVersion>3.0.0.0</AssemblyVersion>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<AssemblyName>JsonSchema.Net</AssemblyName>
Expand Down
8 changes: 8 additions & 0 deletions JsonSchema/ValidationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ public ILog Log
/// </remarks>
public bool OnlyKnownFormats { get; set; }

/// <summary>
/// Specifies whether custom keywords that aren't defined in vocabularies
/// should be processed. Only applies to vocab-enabled JSON Schema versions
/// (e.g. draft 2019-09 &amp; 20200-12). Default is false.
/// </summary>
public bool ProcessCustomKeywords { get; set; }

internal Draft ValidatingAs { get; private set; }

static ValidationOptions()
Expand All @@ -109,6 +116,7 @@ internal static ValidationOptions From(ValidationOptions other)
DefaultBaseUri = other.DefaultBaseUri,
ValidateMetaSchema = other.ValidateMetaSchema,
RequireFormatValidation = other.RequireFormatValidation,
ProcessCustomKeywords = other.ProcessCustomKeywords,
LogIndentLevel = other.LogIndentLevel,
Log = other._log ?? Default.Log,
OnlyKnownFormats = other.OnlyKnownFormats,
Expand Down
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 @@
# [3.3.0](https://github.com/gregsdennis/json-everything/pull/344)

[#340](https://github.com/gregsdennis/json-everything/issues/340) - Added `ValidationOptions.ProcessCustomKeywords` to allow custom keywords for schema versions 2019-09 and later.

# [3.2.1](https://github.com/gregsdennis/json-everything/pull/330)

Fixed absolute schema location in output. The JSON Schema team identified some edge cases involving `$dynamicRef` where the wrong URI was reported.
Expand Down
1 change: 1 addition & 0 deletions json-everything.net/wwwroot/md/schema-basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ The `ValidationOptions` class gives you a few configuration points for customizi
- `ValidateAs` - Indicates which schema draft to process as. This will filter the keywords of a schema based on their support. This means that if any keyword is not supported by this draft, it will be ignored.
- `ValidateMetaSchema` - Indicates whether the schema should be validated against its `$schema` value (its meta-schema). This is not typically necessary.
- `OutputFormat` - You already read about output formats above. This is the property that controls it all. By default, a single "flag" node is returned. This also yields the fastest validation times it enables certain optimizations.
- `ProcessCustomKeywords` - For schema versions which support the vocabulary system (i.g. 2019-09 and after), allows custom keywords to be processed which haven't been included in a vocabulary. This still requires the keyword type to be registered with `SchemaRegistry`.

# Managing references (`$ref`)

Expand Down
2 changes: 1 addition & 1 deletion ref-repos/JSON-Schema-Test-Suite

0 comments on commit 37d3aac

Please sign in to comment.