diff --git a/src/Json.Schema.Validation.UnitTests/ValidatorTests.cs b/src/Json.Schema.Validation.UnitTests/ValidatorTests.cs index 8be5b01..a6c9e31 100644 --- a/src/Json.Schema.Validation.UnitTests/ValidatorTests.cs +++ b/src/Json.Schema.Validation.UnitTests/ValidatorTests.cs @@ -1064,9 +1064,101 @@ public void Serialize(IXunitSerializationInfo info) } }", MakeErrorMessage(2, 8, "a", ErrorNumber.DependentPropertyMissing, "x", "\"y\"", "\"y\"") + ), + new TestCase( + "String: format: valid with date-time format constraints", + @"{ + ""type"": ""string"", + ""format"": ""date-time"" + }", + "\"2023-02-03T12:34:56.789Z\"" + ), + new TestCase( + "String: format: invalid date-time format string", + @"{ + ""type"": ""string"", + ""format"": ""date-time"" + }", + "\"failure-string\"", + MakeErrorMessage(1, 16, string.Empty, ErrorNumber.StringDoesNotMatchFormat, FormatAttributes.DateTime, "failure-string") + ), + new TestCase( + "String: format: valid with uri format constraint", + @"{ + ""type"": ""string"", + ""format"": ""uri"" + }", + "\"https://www.example.com\"" + ), + new TestCase( + "String: format: valid with file uri format constraint", + @"{ + ""type"": ""string"", + ""format"": ""uri"" + }", + "\"file://localhost/absolute/path/to/file\"" + ), + new TestCase( + "String: format: valid with email uri format constraint", + @"{ + ""type"": ""string"", + ""format"": ""uri"" + }", + "\"mailto:jsmith@example.com?subject=A%20Test&body=My%20idea%20is%3A%20%0A\"" + ), + new TestCase( + "String: format: Invalid uri value", + @"{ + ""type"": ""string"", + ""format"": ""uri"" + }", + "\"sites/files/images/picture.png\"", + MakeErrorMessage(1, 32, string.Empty, ErrorNumber.StringDoesNotMatchFormat, FormatAttributes.Uri, "sites/files/images/picture.png") + ), + new TestCase( + "String: format: valid with uri-reference format constraint", + @"{ + ""type"": ""string"", + ""format"": ""uri-reference"" + }", + "\"sites/files/images/picture.png\"" + ), + new TestCase( + "String: format: Invalid with uri-reference format constraint", + @"{ + ""type"": ""string"", + ""format"": ""uri-reference"" + }", + "\"````\"", + MakeErrorMessage(1, 6, string.Empty, ErrorNumber.StringDoesNotMatchFormat, FormatAttributes.UriReference, "````") + ), + new TestCase( + "String: format: valid with uuid format constraint - lowercase", + @"{ + ""type"": ""string"", + ""format"": ""uuid"" + }", + "\"8b524014-4666-449e-afc4-1b82fbfb27ad\"" + ), + new TestCase( + "String: format: valid with uuid format constraint - uppercase", + @"{ + ""type"": ""string"", + ""format"": ""uuid"" + }", + "\"6303D187-07A8-4164-864F-0C01D6F8E5C4\"" + ), + new TestCase( + "String: format: Invalid with uuid format constraint", + @"{ + ""type"": ""string"", + ""format"": ""uuid"" + }", + "\"4014-4666-449e-afc4-1b82fbfb27ad\"", + MakeErrorMessage(1, 34, string.Empty, ErrorNumber.StringDoesNotMatchFormat, FormatAttributes.Uuid, "4014-4666-449e-afc4-1b82fbfb27ad") ) }; - + [Theory(DisplayName = "Validation")] [MemberData(nameof(TestCases))] public void Tests(TestCase test) diff --git a/src/Json.Schema.Validation/RuleFactory.cs b/src/Json.Schema.Validation/RuleFactory.cs index 39c67d9..7861cc0 100644 --- a/src/Json.Schema.Validation/RuleFactory.cs +++ b/src/Json.Schema.Validation/RuleFactory.cs @@ -198,7 +198,12 @@ private static ReportingDescriptor MakeRule(ErrorNumber errorNumber, string full [ErrorNumber.DependentPropertyMissing] = MakeRule( ErrorNumber.DependentPropertyMissing, RuleResources.RuleDescriptionDependentPropertyMissing, - RuleResources.ErrorDependentPropertyMissing) + RuleResources.ErrorDependentPropertyMissing), + + [ErrorNumber.StringDoesNotMatchFormat] = MakeRule( + ErrorNumber.StringDoesNotMatchFormat, + RuleResources.RuleDescriptionStringDoesNotMatchFormat, + RuleResources.ErrorStringDoesNotMatchFormat) }); public static ReportingDescriptor GetRuleFromRuleId(string ruleId) diff --git a/src/Json.Schema.Validation/RuleResources.Designer.cs b/src/Json.Schema.Validation/RuleResources.Designer.cs index b641467..bf32a52 100644 --- a/src/Json.Schema.Validation/RuleResources.Designer.cs +++ b/src/Json.Schema.Validation/RuleResources.Designer.cs @@ -357,7 +357,18 @@ internal static string ErrorWrongType { return ResourceManager.GetString("ErrorWrongType", resourceCulture); } } - + + /// + /// Looks up a localized string similar to The schema requires string value of the format "{1}", but a token value "{2}" was found... + /// + internal static string ErrorStringDoesNotMatchFormat + { + get + { + return ResourceManager.GetString("ErrorStringDoesNotMatchFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to An object contains a property not defined by the schema is present, and the schema does not permit additional properties.. /// @@ -645,5 +656,16 @@ internal static string RuleDescriptionWrongType { return ResourceManager.GetString("RuleDescriptionWrongType", resourceCulture); } } + + /// + /// Looks up a localized string similar to An string value has a format that is not permitted by the schema's "format" property.. + /// + internal static string RuleDescriptionStringDoesNotMatchFormat + { + get + { + return ResourceManager.GetString("RuleDescriptionStringDoesNotMatchFormat", resourceCulture); + } + } } } diff --git a/src/Json.Schema.Validation/RuleResources.resx b/src/Json.Schema.Validation/RuleResources.resx index 366b49e..af7427c 100644 --- a/src/Json.Schema.Validation/RuleResources.resx +++ b/src/Json.Schema.Validation/RuleResources.resx @@ -217,6 +217,9 @@ The schema requires one of the types [{1}], but a token of type '{2}' was found. + + The schema requires string value of the format '{1}', but a token value '{2}' was found. + An object contains a property not defined by the schema, and the schema does not permit additional properties. @@ -313,4 +316,7 @@ An instance has a type that is not permitted by the schema's 'type' property. + + A string value has a format that is not permitted by the schema's 'format' property. + \ No newline at end of file diff --git a/src/Json.Schema.Validation/Validator.cs b/src/Json.Schema.Validation/Validator.cs index 88193f6..40bbd46 100644 --- a/src/Json.Schema.Validation/Validator.cs +++ b/src/Json.Schema.Validation/Validator.cs @@ -181,6 +181,44 @@ private void ValidateString(JValue jValue, JsonSchema schema) AddResult(jValue, ErrorNumber.StringDoesNotMatchPattern, value, schema.Pattern); } } + + if(!string.IsNullOrEmpty(schema.Format)) + { + ValidateStringFormat(jValue, schema.Format); + } + } + + private void ValidateStringFormat(JValue jValue, string format) + { + string value = jValue.Value(); + if (string.Compare(format, FormatAttributes.DateTime) == 0) + { + if (!DateTime.TryParse(value, out DateTime _)) + { + AddResult(jValue, ErrorNumber.StringDoesNotMatchFormat, format, value); + } + } + else if(string.Compare(format, FormatAttributes.Uri) == 0) + { + if (!Uri.IsWellFormedUriString(value, UriKind.Absolute)) + { + AddResult(jValue, ErrorNumber.StringDoesNotMatchFormat, format, value); + } + } + else if(string.Compare(format, FormatAttributes.UriReference) == 0) + { + if (!Uri.IsWellFormedUriString(value, UriKind.RelativeOrAbsolute)) + { + AddResult(jValue, ErrorNumber.StringDoesNotMatchFormat, format, value); + } + } + else if (string.Compare(format, FormatAttributes.Uuid) == 0) + { + if (!Guid.TryParse(value, out Guid _)) + { + AddResult(jValue, ErrorNumber.StringDoesNotMatchFormat, format, value); + } + } } // Compute the length of a string, counting surrogate pairs as one diff --git a/src/Json.Schema/ErrorNumber.cs b/src/Json.Schema/ErrorNumber.cs index 4e8c8e8..c5cc172 100644 --- a/src/Json.Schema/ErrorNumber.cs +++ b/src/Json.Schema/ErrorNumber.cs @@ -474,7 +474,27 @@ public enum ErrorNumber /// The instance contains a property specified by "dependencies", but it does /// not contain all the properties specified by the corresponding property dependency. /// - DependentPropertyMissing = 1023 + DependentPropertyMissing = 1023, + + + /// + /// A string instance does not match the required format. + /// + /// + /// Schema: + /// + /// { + /// "type": "string", + /// "format": "date-time" + /// } + /// + /// + /// Instance: + /// + /// "2023-02-03:T12:00:00Z" + /// + /// + StringDoesNotMatchFormat = 1024, #endregion Errors in instance document }