Skip to content

Commit

Permalink
Merge pull request #606 from gregsdennis/schema/perf-improvements
Browse files Browse the repository at this point in the history
Schema/perf improvements
  • Loading branch information
gregsdennis authored Jan 8, 2024
2 parents 7db2123 + 9d8b0f4 commit f0c4c7e
Show file tree
Hide file tree
Showing 15 changed files with 127 additions and 15 deletions.
4 changes: 3 additions & 1 deletion JsonPath/Expressions/FunctionExpressionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,14 @@ public static bool TryParseFunction(ReadOnlySpan<char> source, ref int index, [N

if (parameterTypeList[parameterIndex] == FunctionType.Value)
{
if (!ValueExpressionParser.TryParse(source, ref i, out var expr, options))
if (!ValueExpressionParser.TryParse(source, ref i, out var expr, options) ||
expr is PathExpressionNode { Path.IsSingular: false })
{
arguments = null;
function = null;
return false;
}

arguments.Add(expr);
}
else if (parameterTypeList[parameterIndex] == FunctionType.Logical)
Expand Down
7 changes: 7 additions & 0 deletions JsonPath/IPathFunctionDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ internal interface IReflectiveFunctionDefinition
/// </summary>
public abstract class ValueFunctionDefinition : IReflectiveFunctionDefinition, IPathFunctionDefinition
{
private class NothingValue{}

/// <summary>
/// Represents the absence of a JSON value and is distinct from any JSON value, including null.
/// </summary>
public static JsonValue Nothing { get; } = JsonValue.Create(new NothingValue())!;

/// <summary>
/// Gets the function name.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions JsonPath/JsonPath.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
<PackageIcon>json-logo-256.png</PackageIcon>
<PackageTags>json-path jsonpath query path json</PackageTags>
<PackageReleaseNotes>Release notes can be found at https://docs.json-everything.net/rn-json-path/</PackageReleaseNotes>
<Version>0.7.0</Version>
<FileVersion>0.7.0.0</FileVersion>
<Version>0.7.1</Version>
<FileVersion>0.7.1.0</FileVersion>
<AssemblyVersion>0.7.0.0</AssemblyVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<DocumentationFile>JsonPath.Net.xml</DocumentationFile>
Expand Down
2 changes: 1 addition & 1 deletion JsonPath/LengthFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class LengthFunction : ValueFunctionDefinition
JsonObject obj => (JsonValue)obj.Count,
JsonArray arr => (JsonValue)arr.Count,
JsonValue val when val.TryGetValue(out string? s) => (JsonValue)new StringInfo(s).LengthInTextElements,
_ => null
_ => Nothing
};
}
}
1 change: 1 addition & 0 deletions JsonPath/NodeList.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;

namespace Json.Path;

Expand Down
2 changes: 1 addition & 1 deletion JsonPath/ValueFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ public class ValueFunction : ValueFunctionDefinition
{
if (nodeList.Count == 1) return nodeList[0].Value;

return null;
return Nothing;
}
}
78 changes: 78 additions & 0 deletions JsonSchema.Tests/Serialization/DeserializationTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Drawing;
using System.Text.Encodings.Web;
using System.Text.Json;
using Json.Schema.Serialization;
Expand Down Expand Up @@ -41,13 +42,28 @@ private class FooWithSchema
)
.Required(nameof(Foo.Value));

public static readonly JsonSchema PointSchema =
new JsonSchemaBuilder()
.Type(SchemaValueType.Object)
.Properties(
("X", new JsonSchemaBuilder().Type(SchemaValueType.Integer)),
("Y", new JsonSchemaBuilder().Type(SchemaValueType.Integer))
)
.AdditionalProperties(false);

private static readonly JsonSerializerOptions _options = new()
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Converters = { new ValidatingJsonConverter { OutputFormat = OutputFormat.List } }
};

[SetUp]
public void Setup()
{
ValidatingJsonConverter.MapType<Point>(PointSchema);
}

/// <summary>
/// Demonstrates that even without a schema, the incorrect JSON data type is
/// caught by the serializer. However, the error is only thrown for the first
Expand Down Expand Up @@ -291,6 +307,68 @@ public void WithSchema_MultipleDeserializations()
throw;
}
}

[Test]
public void InaccessibleType_Valid()
{
try
{
var jsonText = @"{
""X"": 4,
""Y"": 5
}";

var options = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Converters = { new ValidatingJsonConverter { OutputFormat = OutputFormat.List } }
};

var model = JsonSerializer.Deserialize<Point>(jsonText, options);

Console.WriteLine(JsonSerializer.Serialize(model, options));
}
catch (Exception e)
{
HandleException(e);
throw;
}
}

[Test]
public void InaccessibleType_Invalid()
{
Assert.Throws<JsonException>(() =>
{
try
{
var jsonText = @"{
""X"": ""string"",
""Y"": 5
}";

var options = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Converters = { new ValidatingJsonConverter { OutputFormat = OutputFormat.List } }
};

var model = JsonSerializer.Deserialize<Point>(jsonText, options);

Console.WriteLine(JsonSerializer.Serialize(model, options));
}
catch (Exception e)
{
HandleException(e);
Assert.AreEqual("JSON does not meet schema requirements", e.Message);
throw;
}
}
);
}

/// <summary>
/// The validation result is passed in the <see cref="Exception.Data"/>
/// property under the `"validation"` key.
Expand Down
3 changes: 2 additions & 1 deletion JsonSchema/ErrorMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace Json.Schema;
public static partial class ErrorMessages
{
private static readonly ResourceManager _resourceManager = new("Json.Schema.Localization.Resources", typeof(ErrorMessages).Assembly);
private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

/// <summary>
/// Gets or sets a culture to use for error messages. Default is <see cref="CultureInfo.CurrentCulture"/>.
Expand Down Expand Up @@ -52,7 +53,7 @@ public static string ReplaceTokens(this string message, params (string token, ob
for (var i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
values[i] = JsonSerializer.Serialize(parameter.value, new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
values[i] = JsonSerializer.Serialize(parameter.value, _jsonSerializerOptions);
current = current.Replace($"[[{parameter.token}]]", $"{{{i}}}");
}

Expand Down
7 changes: 4 additions & 3 deletions JsonSchema/Formats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public static class Formats
"HH':'mm':'ssK",
"HH':'mm':'ss"
};

//from built from https://regex101.com/r/qH0sU7/1, edited to support all date+time examples in https://ijmacd.github.io/rfc3339-iso8601/
private static readonly Regex _dateTimeRegex = new Regex(@"^((?:(\d{4}-\d{2}-\d{2})([Tt_]| )(\d{2}:\d{2}:\d{2}(?:\.\d+)?))([Zz]|[\+-]\d{2}:\d{2}))$");

/// <summary>
/// Defines the `date` format.
Expand Down Expand Up @@ -271,9 +274,7 @@ private static bool CheckDateFormat(JsonNode? node, params string[] formats)
//date-times with very high precision don't get matched by TryParseExact but are still actually parsable.
//We use a fallback to catch these cases

//from built from https://regex101.com/r/qH0sU7/1, edited to support all date+time examples in https://ijmacd.github.io/rfc3339-iso8601/
var regex = new Regex(@"^((?:(\d{4}-\d{2}-\d{2})([Tt_]| )(\d{2}:\d{2}:\d{2}(?:\.\d+)?))([Zz]|[\+-]\d{2}:\d{2}))$");
var match = regex.Match(dateString);
var match = _dateTimeRegex.Match(dateString);
return match.Success;

}
Expand Down
6 changes: 4 additions & 2 deletions JsonSchema/JsonSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ namespace Json.Schema;
public class JsonSchema : IBaseDocument
{
private const string _unknownKeywordsAnnotationKey = "$unknownKeywords";


private static readonly HashSet<SpecVersion> _definedSpecVersions = new HashSet<SpecVersion>(Enum.GetValues(typeof(SpecVersion)).Cast<SpecVersion>());

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

Expand Down Expand Up @@ -405,7 +407,7 @@ private static SpecVersion DetermineSpecVersion(JsonSchema schema, SchemaRegistr
{
if (schema.BoolValue.HasValue) return SpecVersion.DraftNext;
if (schema.DeclaredVersion != SpecVersion.Unspecified) return schema.DeclaredVersion;
if (!Enum.IsDefined(typeof(SpecVersion), desiredDraft)) return desiredDraft;
if (!_definedSpecVersions.Contains(desiredDraft)) return desiredDraft;

if (schema.TryGetKeyword<SchemaKeyword>(SchemaKeyword.Name, out var schemaKeyword))
{
Expand Down
4 changes: 2 additions & 2 deletions JsonSchema/JsonSchema.csproj
Original file line number Diff line number Diff line change
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.4.3</Version>
<FileVersion>5.4.3.0</FileVersion>
<Version>5.5.0</Version>
<FileVersion>5.5.0.0</FileVersion>
<AssemblyVersion>5.0.0.0</AssemblyVersion>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<AssemblyName>JsonSchema.Net</AssemblyName>
Expand Down
14 changes: 13 additions & 1 deletion JsonSchema/Serialization/ValidatingJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace Json.Schema.Serialization;
public class ValidatingJsonConverter : JsonConverterFactory
{
private static readonly ConcurrentDictionary<Type, JsonConverter?> _cache = new();
private static readonly ValidatingJsonConverter _instance = new();

/// <summary>
/// Specifies the output format.
Expand All @@ -29,6 +30,11 @@ public class ValidatingJsonConverter : JsonConverterFactory
/// </summary>
public bool? RequireFormatValidation { get; set; }

public static void MapType<T>(JsonSchema schema)

Check warning on line 33 in JsonSchema/Serialization/ValidatingJsonConverter.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'ValidatingJsonConverter.MapType<T>(JsonSchema)'

Check warning on line 33 in JsonSchema/Serialization/ValidatingJsonConverter.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'ValidatingJsonConverter.MapType<T>(JsonSchema)'

Check warning on line 33 in JsonSchema/Serialization/ValidatingJsonConverter.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'ValidatingJsonConverter.MapType<T>(JsonSchema)'

Check warning on line 33 in JsonSchema/Serialization/ValidatingJsonConverter.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'ValidatingJsonConverter.MapType<T>(JsonSchema)'

Check warning on line 33 in JsonSchema/Serialization/ValidatingJsonConverter.cs

View workflow job for this annotation

GitHub Actions / build, pack & publish

Missing XML comment for publicly visible type or member 'ValidatingJsonConverter.MapType<T>(JsonSchema)'

Check warning on line 33 in JsonSchema/Serialization/ValidatingJsonConverter.cs

View workflow job for this annotation

GitHub Actions / build, pack & publish

Missing XML comment for publicly visible type or member 'ValidatingJsonConverter.MapType<T>(JsonSchema)'

Check warning on line 33 in JsonSchema/Serialization/ValidatingJsonConverter.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'ValidatingJsonConverter.MapType<T>(JsonSchema)'

Check warning on line 33 in JsonSchema/Serialization/ValidatingJsonConverter.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'ValidatingJsonConverter.MapType<T>(JsonSchema)'
{
_instance.CreateConverter(typeof(T), schema);
}

/// <summary>When overridden in a derived class, determines whether the converter instance can convert the specified object type.</summary>
/// <param name="typeToConvert">The type of the object to check whether it can be converted by this converter instance.</param>
/// <returns>
Expand Down Expand Up @@ -57,7 +63,13 @@ public override bool CanConvert(Type typeToConvert)
if (_cache.TryGetValue(typeToConvert, out var converter)) return converter;

var schemaAttribute = (JsonSchemaAttribute)typeToConvert.GetCustomAttributes(typeof(JsonSchemaAttribute)).Single();
var schema = schemaAttribute.Schema;

return CreateConverter(typeToConvert, schema);
}

private JsonConverter? CreateConverter(Type typeToConvert, JsonSchema schema)
{
var converterType = typeof(ValidatingJsonConverter<>).MakeGenericType(typeToConvert);

// ReSharper disable once ConvertToLocalFunction
Expand All @@ -71,7 +83,7 @@ public override bool CanConvert(Type typeToConvert)
}
return newOptions;
};
converter = (JsonConverter)Activator.CreateInstance(converterType, schemaAttribute.Schema, optionsFactory);
var converter = (JsonConverter)Activator.CreateInstance(converterType, schema, optionsFactory);

var validatingConverter = (IValidatingJsonConverter)converter;
validatingConverter.OutputFormat = OutputFormat ?? Schema.OutputFormat.Flag;
Expand Down
2 changes: 2 additions & 0 deletions json-everything.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/GrammarAndSpelling/GrammarChecking/RulesStates/=LanguageTool_002EEN_002ELANGUAGE_005FTOOL/@EntryIndexedValue">DisabledByUser</s:String>
<s:String x:Key="/Default/GrammarAndSpelling/GrammarChecking/RulesStates/=LanguageTool_002EEN_002ELANGUAGETOOL/@EntryIndexedValue">DisabledByUser</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=antimultiples/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=codegen/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Contexted/@EntryIndexedValue">True</s:Boolean>
Expand Down
6 changes: 6 additions & 0 deletions tools/ApiDocsGenerator/release-notes/rn-json-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ title: JsonSchema.Net
icon: fas fa-tag
order: "09.01"
---
# [5.5.0](https://github.com/gregsdennis/json-everything/pull/606) {#release-pointer-5.5.0}

[#604](https://github.com/gregsdennis/json-everything/pull/604) - Performance improvements. Thanks to [@danielstarck](https://github.com/danielstarck) for contributing these.

[#605](https://github.com/gregsdennis/json-everything/pull/605) - Support external types for validating JSON converter.

# [5.4.3](https://github.com/gregsdennis/json-everything/pull/601) {#release-pointer-5.4.3}

[#600](https://github.com/gregsdennis/json-everything/issues/600) - Reported schema location not correct for JSON Schemas embedded within non-schema JSON documents (e.g. OpenAPI). Thanks to [@Fresa](https://github.com/Fresa) for reporting.
Expand Down

0 comments on commit f0c4c7e

Please sign in to comment.