diff --git a/Src/Newtonsoft.Json.Tests/JsonValidatingReaderTests.cs b/Src/Newtonsoft.Json.Tests/JsonValidatingReaderTests.cs index 26d7eff0..72556ad1 100644 --- a/Src/Newtonsoft.Json.Tests/JsonValidatingReaderTests.cs +++ b/Src/Newtonsoft.Json.Tests/JsonValidatingReaderTests.cs @@ -976,7 +976,7 @@ namespace Newtonsoft.Json.Tests } [Test] - public void sdfsdf() + public void NoAdditionalProperties() { string schemaJson = @"{ ""type"":""array"", @@ -1013,6 +1013,69 @@ namespace Newtonsoft.Json.Tests Assert.IsFalse(reader.Read()); } + [Test] + public void PatternPropertiesNoAdditionalProperties() + { + string schemaJson = @"{ + ""type"":""object"", + ""patternProperties"": { + ""hi"": {""type"":""string""}, + ""ho"": {""type"":""string""} + }, + ""additionalProperties"": false +}"; + + string json = @"{ + ""hi"": ""A string!"", + ""hide"": ""A string!"", + ""ho"": 1, + ""hey"": ""A string!"" +}"; + + Json.Schema.ValidationEventArgs validationEventArgs = null; + + JsonValidatingReader reader = new JsonValidatingReader(new JsonTextReader(new StringReader(json))); + reader.ValidationEventHandler += (sender, args) => { validationEventArgs = args; }; + reader.Schema = JsonSchema.Parse(schemaJson); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual(JsonToken.StartObject, reader.TokenType); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual(JsonToken.PropertyName, reader.TokenType); + Assert.AreEqual(null, validationEventArgs); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual(JsonToken.String, reader.TokenType); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual(JsonToken.PropertyName, reader.TokenType); + Assert.AreEqual(null, validationEventArgs); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual(JsonToken.String, reader.TokenType); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual(JsonToken.PropertyName, reader.TokenType); + Assert.AreEqual(null, validationEventArgs); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual(JsonToken.Integer, reader.TokenType); + Assert.AreEqual("Invalid type. Expected String but got Integer. Line 4, position 10.", validationEventArgs.Message); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual(JsonToken.PropertyName, reader.TokenType); + Assert.AreEqual("Property 'hey' has not been defined and the schema does not allow additional properties. Line 5, position 8.", validationEventArgs.Message); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual(JsonToken.String, reader.TokenType); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual(JsonToken.EndObject, reader.TokenType); + + Assert.IsFalse(reader.Read()); + } + [Test] public void ExtendedComplex() { diff --git a/Src/Newtonsoft.Json.Tests/Schema/ExtensionsTests.cs b/Src/Newtonsoft.Json.Tests/Schema/ExtensionsTests.cs index 31ed7d9a..2d02d979 100644 --- a/Src/Newtonsoft.Json.Tests/Schema/ExtensionsTests.cs +++ b/Src/Newtonsoft.Json.Tests/Schema/ExtensionsTests.cs @@ -217,5 +217,64 @@ namespace Newtonsoft.Json.Tests.Schema Assert.AreEqual(1, errors.Count); Assert.AreEqual("Property 'g' has not been defined and the schema does not allow additional properties. Line 1, position 5.", errors[0]); } + + [Test] + [ExpectedException(typeof(JsonSchemaException), ExpectedMessage = "Integer 10 equals maximum value of 10 and exclusive maximum is true.")] + public void ExclusiveMaximum_Int() + { + JsonSchema schema = new JsonSchema(); + schema.Maximum = 10; + schema.ExclusiveMaximum = true; + + JValue v = new JValue(10); + v.Validate(schema); + } + + [Test] + [ExpectedException(typeof(JsonSchemaException), ExpectedMessage = "Float 10.1 equals maximum value of 10.1 and exclusive maximum is true.")] + public void ExclusiveMaximum_Float() + { + JsonSchema schema = new JsonSchema(); + schema.Maximum = 10.1; + schema.ExclusiveMaximum = true; + + JValue v = new JValue(10.1); + v.Validate(schema); + } + + [Test] + [ExpectedException(typeof(JsonSchemaException), ExpectedMessage = "Integer 10 equals minimum value of 10 and exclusive minimum is true.")] + public void ExclusiveMinimum_Int() + { + JsonSchema schema = new JsonSchema(); + schema.Minimum = 10; + schema.ExclusiveMinimum = true; + + JValue v = new JValue(10); + v.Validate(schema); + } + + [Test] + [ExpectedException(typeof(JsonSchemaException), ExpectedMessage = "Float 10.1 equals minimum value of 10.1 and exclusive minimum is true.")] + public void ExclusiveMinimum_Float() + { + JsonSchema schema = new JsonSchema(); + schema.Minimum = 10.1; + schema.ExclusiveMinimum = true; + + JValue v = new JValue(10.1); + v.Validate(schema); + } + + [Test] + [ExpectedException(typeof(JsonSchemaException), ExpectedMessage = "Integer 10 is not evenly divisible by 3.")] + public void DivisibleBy_Int() + { + JsonSchema schema = new JsonSchema(); + schema.DivisibleBy = 3; + + JValue v = new JValue(10); + v.Validate(schema); + } } } \ No newline at end of file diff --git a/Src/Newtonsoft.Json.Tests/Schema/JsonSchemaBuilderTests.cs b/Src/Newtonsoft.Json.Tests/Schema/JsonSchemaBuilderTests.cs index b3bba9b5..826f74f4 100644 --- a/Src/Newtonsoft.Json.Tests/Schema/JsonSchemaBuilderTests.cs +++ b/Src/Newtonsoft.Json.Tests/Schema/JsonSchemaBuilderTests.cs @@ -130,6 +130,21 @@ namespace Newtonsoft.Json.Tests.Schema Assert.AreEqual(true, schema.Required); } + [Test] + public void ExclusiveMinimum_ExclusiveMaximum() + { + string json = @"{ + ""exclusiveMinimum"":true, + ""exclusiveMaximum"":true +}"; + + JsonSchemaBuilder builder = new JsonSchemaBuilder(new JsonSchemaResolver()); + JsonSchema schema = builder.Parse(new JsonTextReader(new StringReader(json))); + + Assert.AreEqual(true, schema.ExclusiveMinimum); + Assert.AreEqual(true, schema.ExclusiveMaximum); + } + [Test] public void ReadOnly() { @@ -433,5 +448,22 @@ namespace Newtonsoft.Json.Tests.Schema JsonSchemaBuilder builder = new JsonSchemaBuilder(new JsonSchemaResolver()); JsonSchema schema = builder.Parse(new JsonTextReader(new StringReader(json))); } + + [Test] + public void PatternProperties() + { + string json = @"{ + ""patternProperties"": { + ""[abc]"": { ""id"":""Blah"" } + } +}"; + + JsonSchemaBuilder builder = new JsonSchemaBuilder(new JsonSchemaResolver()); + JsonSchema schema = builder.Parse(new JsonTextReader(new StringReader(json))); + + Assert.IsNotNull(schema.PatternProperties); + Assert.AreEqual(1, schema.PatternProperties.Count); + Assert.AreEqual("Blah", schema.PatternProperties["[abc]"].Id); + } } } diff --git a/Src/Newtonsoft.Json.Tests/Schema/JsonSchemaTests.cs b/Src/Newtonsoft.Json.Tests/Schema/JsonSchemaTests.cs index 7459ce08..0e31a1eb 100644 --- a/Src/Newtonsoft.Json.Tests/Schema/JsonSchemaTests.cs +++ b/Src/Newtonsoft.Json.Tests/Schema/JsonSchemaTests.cs @@ -377,5 +377,50 @@ namespace Newtonsoft.Json.Tests.Schema Assert.AreEqual(schema.Properties["x"].Options[1], "Asc"); Assert.AreEqual(schema.Properties["x"].Options[-1], "Desc"); } + + [Test] + public void WriteTo_ExclusiveMinimum_ExclusiveMaximum() + { + JsonSchema schema = new JsonSchema(); + schema.ExclusiveMinimum = true; + schema.ExclusiveMaximum = true; + + StringWriter writer = new StringWriter(); + JsonTextWriter jsonWriter = new JsonTextWriter(writer); + jsonWriter.Formatting = Formatting.Indented; + + schema.WriteTo(jsonWriter); + + string json = writer.ToString(); + + Assert.AreEqual(@"{ + ""exclusiveMinimum"": true, + ""exclusiveMaximum"": true +}", json); + } + + [Test] + public void WriteTo_PatternProperties() + { + JsonSchema schema = new JsonSchema(); + schema.PatternProperties = new Dictionary + { + { "[abc]", new JsonSchema() } + }; + + StringWriter writer = new StringWriter(); + JsonTextWriter jsonWriter = new JsonTextWriter(writer); + jsonWriter.Formatting = Formatting.Indented; + + schema.WriteTo(jsonWriter); + + string json = writer.ToString(); + + Assert.AreEqual(@"{ + ""patternProperties"": { + ""[abc]"": {} + } +}", json); + } } } diff --git a/Src/Newtonsoft.Json/JsonValidatingReader.cs b/Src/Newtonsoft.Json/JsonValidatingReader.cs index 6fc83cec..5bace6db 100644 --- a/Src/Newtonsoft.Json/JsonValidatingReader.cs +++ b/Src/Newtonsoft.Json/JsonValidatingReader.cs @@ -43,15 +43,15 @@ namespace Newtonsoft.Json private class SchemaScope { private readonly JTokenType _tokenType; - private readonly JsonSchemaModel _schema; + private readonly IList _schemas; private readonly Dictionary _requiredProperties; public string CurrentPropertyName { get; set; } public int ArrayItemCount { get; set; } - public JsonSchemaModel Schema + public IList Schemas { - get { return _schema; } + get { return _schemas; } } public Dictionary RequiredProperties @@ -64,19 +64,19 @@ namespace Newtonsoft.Json get { return _tokenType; } } - public SchemaScope(JTokenType tokenType, JsonSchemaModel schema) + public SchemaScope(JTokenType tokenType, IList schemas) { _tokenType = tokenType; - _schema = schema; + _schemas = schemas; - if (_schema != null && _schema.Properties != null) - _requiredProperties = GetRequiredProperties(_schema).Distinct().ToDictionary(p => p, p => false); - else - _requiredProperties = new Dictionary(); + _requiredProperties = schemas.SelectMany(GetRequiredProperties).Distinct().ToDictionary(p => p, p => false); } private IEnumerable GetRequiredProperties(JsonSchemaModel schema) { + if (schema == null || schema.Properties == null) + return Enumerable.Empty(); + return schema.Properties.Where(p => p.Value.Required).Select(p => p.Key); } } @@ -154,47 +154,79 @@ namespace Newtonsoft.Json return poppedScope; } - private JsonSchemaModel CurrentSchema + private IEnumerable CurrentSchemas { - get { return _currentScope.Schema; } + get { return _currentScope.Schemas; } } - private JsonSchemaModel CurrentMemberSchema + private IEnumerable CurrentMemberSchemas { get { if (_currentScope == null) - return _model; + return new List(new [] { _model }); - if (_currentScope.Schema == null) - return null; + if (_currentScope.Schemas == null || _currentScope.Schemas.Count == 0) + return Enumerable.Empty(); switch (_currentScope.TokenType) { case JTokenType.None: - return _currentScope.Schema; + return _currentScope.Schemas; case JTokenType.Object: - if (_currentScope.CurrentPropertyName == null) - throw new Exception("CurrentPropertyName has not been set on scope."); - - JsonSchemaModel propertySchema; - if (CurrentSchema.Properties != null && CurrentSchema.Properties.TryGetValue(_currentScope.CurrentPropertyName, out propertySchema)) - return propertySchema; - - return (CurrentSchema.AllowAdditionalProperties) ? CurrentSchema.AdditionalProperties : null; - case JTokenType.Array: - if (!CollectionUtils.IsNullOrEmpty(CurrentSchema.Items)) { - if (CurrentSchema.Items.Count == 1) - return CurrentSchema.Items[0]; + if (_currentScope.CurrentPropertyName == null) + throw new Exception("CurrentPropertyName has not been set on scope."); - if (CurrentSchema.Items.Count > (_currentScope.ArrayItemCount - 1)) - return CurrentSchema.Items[_currentScope.ArrayItemCount - 1]; + IList schemas = new List(); + + foreach (JsonSchemaModel schema in CurrentSchemas) + { + JsonSchemaModel propertySchema; + if (schema.Properties != null && schema.Properties.TryGetValue(_currentScope.CurrentPropertyName, out propertySchema)) + { + schemas.Add(propertySchema); + } + if (schema.PatternProperties != null) + { + foreach (KeyValuePair patternProperty in schema.PatternProperties) + { + if (Regex.IsMatch(_currentScope.CurrentPropertyName, patternProperty.Key)) + { + schemas.Add(patternProperty.Value); + } + } + } + + if (schemas.Count == 0 && schema.AllowAdditionalProperties && schema.AdditionalProperties != null) + schemas.Add(schema.AdditionalProperties); + } + + return schemas; } + case JTokenType.Array: + { + IList schemas = new List(); + + foreach (JsonSchemaModel schema in CurrentSchemas) + { + if (!CollectionUtils.IsNullOrEmpty(schema.Items)) + { + if (schema.Items.Count == 1) + schemas.Add(schema.Items[0]); - return (CurrentSchema.AllowAdditionalProperties) ? CurrentSchema.AdditionalProperties : null; + if (schema.Items.Count > (_currentScope.ArrayItemCount - 1)) + schemas.Add(schema.Items[_currentScope.ArrayItemCount - 1]); + } + + if (schema.AllowAdditionalProperties && schema.AdditionalProperties != null) + schemas.Add(schema.AdditionalProperties); + } + + return schemas; + } case JTokenType.Constructor: - return null; + return Enumerable.Empty(); default: throw new ArgumentOutOfRangeException("TokenType", "Unexpected token type: {0}".FormatWith(CultureInfo.InvariantCulture, _currentScope.TokenType)); } @@ -371,58 +403,80 @@ namespace Newtonsoft.Json _model = builder.Build(_schema); } + //ValidateValueToken(); + switch (_reader.TokenType) { case JsonToken.StartObject: ProcessValue(); - JsonSchemaModel objectSchema = (ValidateObject(CurrentMemberSchema)) - ? CurrentMemberSchema - : null; - Push(new SchemaScope(JTokenType.Object, objectSchema)); + IList objectSchemas = CurrentMemberSchemas.Where(ValidateObject).ToList(); + Push(new SchemaScope(JTokenType.Object, objectSchemas)); break; case JsonToken.StartArray: ProcessValue(); - JsonSchemaModel arraySchema = (ValidateArray(CurrentMemberSchema)) - ? CurrentMemberSchema - : null; - Push(new SchemaScope(JTokenType.Array, arraySchema)); + IList arraySchemas = CurrentMemberSchemas.Where(ValidateArray).ToList(); + Push(new SchemaScope(JTokenType.Array, arraySchemas)); break; case JsonToken.StartConstructor: Push(new SchemaScope(JTokenType.Constructor, null)); break; case JsonToken.PropertyName: - ValidatePropertyName(CurrentSchema); + foreach (JsonSchemaModel schema in CurrentSchemas) + { + ValidatePropertyName(schema); + } break; case JsonToken.Raw: break; case JsonToken.Integer: ProcessValue(); - ValidateInteger(CurrentMemberSchema); + foreach (JsonSchemaModel schema in CurrentMemberSchemas) + { + ValidateInteger(schema); + } break; case JsonToken.Float: ProcessValue(); - ValidateFloat(CurrentMemberSchema); + foreach (JsonSchemaModel schema in CurrentMemberSchemas) + { + ValidateFloat(schema); + } break; case JsonToken.String: ProcessValue(); - ValidateString(CurrentMemberSchema); + foreach (JsonSchemaModel schema in CurrentMemberSchemas) + { + ValidateString(schema); + } break; case JsonToken.Boolean: ProcessValue(); - ValidateBoolean(CurrentMemberSchema); + foreach (JsonSchemaModel schema in CurrentMemberSchemas) + { + ValidateBoolean(schema); + } break; case JsonToken.Null: ProcessValue(); - ValidateNull(CurrentMemberSchema); + foreach (JsonSchemaModel schema in CurrentMemberSchemas) + { + ValidateNull(schema); + } break; case JsonToken.Undefined: break; case JsonToken.EndObject: - ValidateEndObject(CurrentSchema); + foreach (JsonSchemaModel schema in CurrentSchemas) + { + ValidateEndObject(schema); + } Pop(); break; case JsonToken.EndArray: - ValidateEndArray(CurrentSchema); + foreach (JsonSchemaModel schema in CurrentSchemas) + { + ValidateEndArray(schema); + } Pop(); break; case JsonToken.EndConstructor: @@ -528,11 +582,24 @@ namespace Newtonsoft.Json long value = Convert.ToInt64(_reader.Value, CultureInfo.InvariantCulture); - if (schema.Maximum != null && value > schema.Maximum) - RaiseError("Integer {0} exceeds maximum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Maximum), schema); + if (schema.Maximum != null) + { + if (value > schema.Maximum) + RaiseError("Integer {0} exceeds maximum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Maximum), schema); + if (schema.ExclusiveMaximum && value == schema.Maximum) + RaiseError("Integer {0} equals maximum value of {1} and exclusive maximum is true.".FormatWith(CultureInfo.InvariantCulture, value, schema.Maximum), schema); + } - if (schema.Minimum != null && value < schema.Minimum) - RaiseError("Integer {0} is less than minimum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Minimum), schema); + if (schema.Minimum != null) + { + if (value < schema.Minimum) + RaiseError("Integer {0} is less than minimum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Minimum), schema); + if (schema.ExclusiveMinimum && value == schema.Minimum) + RaiseError("Integer {0} equals minimum value of {1} and exclusive minimum is true.".FormatWith(CultureInfo.InvariantCulture, value, schema.Minimum), schema); + } + + if (schema.DivisibleBy != null && !IsZero(value % schema.DivisibleBy.Value)) + RaiseError("Integer {0} is not evenly divisible by {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.DivisibleBy), schema); } private void ProcessValue() @@ -541,8 +608,11 @@ namespace Newtonsoft.Json { _currentScope.ArrayItemCount++; - if (CurrentSchema != null && CurrentSchema.Items != null && CurrentSchema.Items.Count > 1 && _currentScope.ArrayItemCount >= CurrentSchema.Items.Count) - RaiseError("Index {0} has not been defined and the schema does not allow additional items.".FormatWith(CultureInfo.InvariantCulture, _currentScope.ArrayItemCount), CurrentSchema); + foreach (JsonSchemaModel currentSchema in CurrentSchemas) + { + if (currentSchema != null && currentSchema.Items != null && currentSchema.Items.Count > 1 && _currentScope.ArrayItemCount >= currentSchema.Items.Count) + RaiseError("Index {0} has not been defined and the schema does not allow additional items.".FormatWith(CultureInfo.InvariantCulture, _currentScope.ArrayItemCount), currentSchema); + } } } @@ -558,11 +628,21 @@ namespace Newtonsoft.Json double value = Convert.ToDouble(_reader.Value, CultureInfo.InvariantCulture); - if (schema.Maximum != null && value > schema.Maximum) - RaiseError("Float {0} exceeds maximum value of {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Maximum), schema); + if (schema.Maximum != null) + { + if (value > schema.Maximum) + RaiseError("Float {0} exceeds maximum value of {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Maximum), schema); + if (schema.ExclusiveMaximum && value == schema.Maximum) + RaiseError("Float {0} equals maximum value of {1} and exclusive maximum is true.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Maximum), schema); + } - if (schema.Minimum != null && value < schema.Minimum) - RaiseError("Float {0} is less than minimum value of {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Minimum), schema); + if (schema.Minimum != null) + { + if (value < schema.Minimum) + RaiseError("Float {0} is less than minimum value of {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Minimum), schema); + if (schema.ExclusiveMinimum && value == schema.Minimum) + RaiseError("Float {0} equals minimum value of {1} and exclusive minimum is true.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Minimum), schema); + } if (schema.DivisibleBy != null && !IsZero(value % schema.DivisibleBy.Value)) RaiseError("Float {0} is not evenly divisible by {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.DivisibleBy), schema); @@ -585,14 +665,34 @@ namespace Newtonsoft.Json if (_currentScope.RequiredProperties.ContainsKey(propertyName)) _currentScope.RequiredProperties[propertyName] = true; - bool propertyDefinied = (schema.Properties != null && schema.Properties.ContainsKey(propertyName)); + if (!schema.AllowAdditionalProperties) + { + bool propertyDefinied = IsPropertyDefinied(schema, propertyName); - if (!propertyDefinied && !schema.AllowAdditionalProperties) - RaiseError("Property '{0}' has not been defined and the schema does not allow additional properties.".FormatWith(CultureInfo.InvariantCulture, propertyName), schema); + if (!propertyDefinied) + RaiseError("Property '{0}' has not been defined and the schema does not allow additional properties.".FormatWith(CultureInfo.InvariantCulture, propertyName), schema); + } _currentScope.CurrentPropertyName = propertyName; } + private bool IsPropertyDefinied(JsonSchemaModel schema, string propertyName) + { + if (schema.Properties != null && schema.Properties.ContainsKey(propertyName)) + return true; + + if (schema.PatternProperties != null) + { + foreach (string pattern in schema.PatternProperties.Keys) + { + if (Regex.IsMatch(propertyName, pattern)) + return true; + } + } + + return false; + } + private bool ValidateArray(JsonSchemaModel schema) { if (schema == null) diff --git a/Src/Newtonsoft.Json/Schema/JsonSchema.cs b/Src/Newtonsoft.Json/Schema/JsonSchema.cs index c5c752f8..5eeb88b5 100644 --- a/Src/Newtonsoft.Json/Schema/JsonSchema.cs +++ b/Src/Newtonsoft.Json/Schema/JsonSchema.cs @@ -89,9 +89,9 @@ namespace Newtonsoft.Json.Schema /// The maximum length. public int? MaximumLength { get; set; } /// - /// Gets or sets the maximum decimals. + /// Gets or sets a number that the value should be divisble by. /// - /// The maximum decimals. + /// A number that the value should be divisble by. public double? DivisibleBy { get; set; } /// /// Gets or sets the minimum. @@ -104,6 +104,16 @@ namespace Newtonsoft.Json.Schema /// The maximum. public double? Maximum { get; set; } /// + /// Gets or sets a flag indicating whether the value can not equal the number defined by the "minimum" attribute. + /// + /// A flag indicating whether the value can not equal the number defined by the "minimum" attribute. + public bool? ExclusiveMinimum { get; set; } + /// + /// Gets or sets a flag indicating whether the value can not equal the number defined by the "maximum" attribute. + /// + /// A flag indicating whether the value can not equal the number defined by the "maximum" attribute. + public bool? ExclusiveMaximum { get; set; } + /// /// Gets or sets the minimum number of items. /// /// The minimum number of items. @@ -129,6 +139,11 @@ namespace Newtonsoft.Json.Schema /// The of additional properties. public JsonSchema AdditionalProperties { get; set; } /// + /// Gets or sets the pattern properties. + /// + /// The pattern properties. + public IDictionary PatternProperties { get; set; } + /// /// Gets or sets a value indicating whether additional properties are allowed. /// /// diff --git a/Src/Newtonsoft.Json/Schema/JsonSchemaBuilder.cs b/Src/Newtonsoft.Json/Schema/JsonSchemaBuilder.cs index e5e5ad41..830adbe8 100644 --- a/Src/Newtonsoft.Json/Schema/JsonSchemaBuilder.cs +++ b/Src/Newtonsoft.Json/Schema/JsonSchemaBuilder.cs @@ -146,6 +146,9 @@ namespace Newtonsoft.Json.Schema case JsonSchemaConstants.AdditionalPropertiesPropertyName: ProcessAdditionalProperties(); break; + case JsonSchemaConstants.PatternPropertiesPropertyName: + ProcessPatternProperties(); + break; case JsonSchemaConstants.RequiredPropertyName: CurrentSchema.Required = (bool)_reader.Value; break; @@ -161,6 +164,12 @@ namespace Newtonsoft.Json.Schema case JsonSchemaConstants.MaximumPropertyName: CurrentSchema.Maximum = Convert.ToDouble(_reader.Value, CultureInfo.InvariantCulture); break; + case JsonSchemaConstants.ExclusiveMinimumPropertyName: + CurrentSchema.ExclusiveMinimum = (bool)_reader.Value; + break; + case JsonSchemaConstants.ExclusiveMaximumPropertyName: + CurrentSchema.ExclusiveMaximum = (bool)_reader.Value; + break; case JsonSchemaConstants.MaximumLengthPropertyName: CurrentSchema.MaximumLength = Convert.ToInt32(_reader.Value, CultureInfo.InvariantCulture); break; @@ -311,6 +320,27 @@ namespace Newtonsoft.Json.Schema CurrentSchema.AdditionalProperties = BuildSchema(); } + private void ProcessPatternProperties() + { + Dictionary patternProperties = new Dictionary(); + + if (_reader.TokenType != JsonToken.StartObject) + throw new Exception("Expected start object token."); + + while (_reader.Read() && _reader.TokenType != JsonToken.EndObject) + { + string propertyName = Convert.ToString(_reader.Value, CultureInfo.InvariantCulture); + _reader.Read(); + + if (patternProperties.ContainsKey(propertyName)) + throw new Exception("Property {0} has already been defined in schema.".FormatWith(CultureInfo.InvariantCulture, propertyName)); + + patternProperties.Add(propertyName, BuildSchema()); + } + + CurrentSchema.PatternProperties = patternProperties; + } + private void ProcessItems() { CurrentSchema.Items = new List(); diff --git a/Src/Newtonsoft.Json/Schema/JsonSchemaConstants.cs b/Src/Newtonsoft.Json/Schema/JsonSchemaConstants.cs index 906b45fe..0e9eda1e 100644 --- a/Src/Newtonsoft.Json/Schema/JsonSchemaConstants.cs +++ b/Src/Newtonsoft.Json/Schema/JsonSchemaConstants.cs @@ -36,11 +36,14 @@ namespace Newtonsoft.Json.Schema public const string PropertiesPropertyName = "properties"; public const string ItemsPropertyName = "items"; public const string RequiredPropertyName = "required"; + public const string PatternPropertiesPropertyName = "patternProperties"; public const string AdditionalPropertiesPropertyName = "additionalProperties"; public const string RequiresPropertyName = "requires"; public const string IdentityPropertyName = "identity"; public const string MinimumPropertyName = "minimum"; public const string MaximumPropertyName = "maximum"; + public const string ExclusiveMinimumPropertyName = "exclusiveMinimum"; + public const string ExclusiveMaximumPropertyName = "exclusiveMaximum"; public const string MinimumItemsPropertyName = "minItems"; public const string MaximumItemsPropertyName = "maxItems"; public const string PatternPropertyName = "pattern"; diff --git a/Src/Newtonsoft.Json/Schema/JsonSchemaModel.cs b/Src/Newtonsoft.Json/Schema/JsonSchemaModel.cs index c817669e..0d438b4b 100644 --- a/Src/Newtonsoft.Json/Schema/JsonSchemaModel.cs +++ b/Src/Newtonsoft.Json/Schema/JsonSchemaModel.cs @@ -40,11 +40,14 @@ namespace Newtonsoft.Json.Schema public double? DivisibleBy { get; set; } public double? Minimum { get; set; } public double? Maximum { get; set; } + public bool ExclusiveMinimum { get; set; } + public bool ExclusiveMaximum { get; set; } public int? MinimumItems { get; set; } public int? MaximumItems { get; set; } public IList Patterns { get; set; } public IList Items { get; set; } public IDictionary Properties { get; set; } + public IDictionary PatternProperties { get; set; } public JsonSchemaModel AdditionalProperties { get; set; } public bool AllowAdditionalProperties { get; set; } public IList Enum { get; set; } @@ -83,6 +86,9 @@ namespace Newtonsoft.Json.Schema model.Minimum = MathUtils.Max(model.Minimum, schema.Minimum); model.Maximum = MathUtils.Max(model.Maximum, schema.Maximum); + model.ExclusiveMinimum = model.ExclusiveMinimum || (schema.ExclusiveMinimum ?? false); + model.ExclusiveMaximum = model.ExclusiveMaximum || (schema.ExclusiveMaximum ?? false); + model.MinimumItems = MathUtils.Max(model.MinimumItems, schema.MinimumItems); model.MaximumItems = MathUtils.Min(model.MaximumItems, schema.MaximumItems); model.AllowAdditionalProperties = model.AllowAdditionalProperties && schema.AllowAdditionalProperties; diff --git a/Src/Newtonsoft.Json/Schema/JsonSchemaModelBuilder.cs b/Src/Newtonsoft.Json/Schema/JsonSchemaModelBuilder.cs index 5c7d3b29..ba3c7505 100644 --- a/Src/Newtonsoft.Json/Schema/JsonSchemaModelBuilder.cs +++ b/Src/Newtonsoft.Json/Schema/JsonSchemaModelBuilder.cs @@ -71,13 +71,9 @@ namespace Newtonsoft.Json.Schema _nodes.Add(currentNode); - if (schema.Properties != null) - { - foreach (KeyValuePair property in schema.Properties) - { - AddProperty(currentNode, property.Key, property.Value); - } - } + AddProperties(schema.Properties, currentNode.Properties); + + AddProperties(schema.PatternProperties, currentNode.PatternProperties); if (schema.Items != null) { @@ -96,12 +92,23 @@ namespace Newtonsoft.Json.Schema return currentNode; } - public void AddProperty(JsonSchemaNode parentNode, string propertyName, JsonSchema schema) + public void AddProperties(IDictionary source, IDictionary target) + { + if (source != null) + { + foreach (KeyValuePair property in source) + { + AddProperty(target, property.Key, property.Value); + } + } + } + + public void AddProperty(IDictionary target, string propertyName, JsonSchema schema) { JsonSchemaNode propertyNode; - parentNode.Properties.TryGetValue(propertyName, out propertyNode); + target.TryGetValue(propertyName, out propertyNode); - parentNode.Properties[propertyName] = AddSchema(propertyNode, schema); + target[propertyName] = AddSchema(propertyNode, schema); } public void AddItem(JsonSchemaNode parentNode, int index, JsonSchema schema) @@ -143,6 +150,13 @@ namespace Newtonsoft.Json.Schema model.Properties[property.Key] = BuildNodeModel(property.Value); } + foreach (KeyValuePair property in node.PatternProperties) + { + if (model.PatternProperties == null) + model.PatternProperties = new Dictionary(); + + model.PatternProperties[property.Key] = BuildNodeModel(property.Value); + } for (int i = 0; i < node.Items.Count; i++) { if (model.Items == null) diff --git a/Src/Newtonsoft.Json/Schema/JsonSchemaNode.cs b/Src/Newtonsoft.Json/Schema/JsonSchemaNode.cs index 33f45588..5f1a23c9 100644 --- a/Src/Newtonsoft.Json/Schema/JsonSchemaNode.cs +++ b/Src/Newtonsoft.Json/Schema/JsonSchemaNode.cs @@ -35,6 +35,7 @@ namespace Newtonsoft.Json.Schema public string Id { get; private set; } public ReadOnlyCollection Schemas { get; private set; } public Dictionary Properties { get; private set; } + public Dictionary PatternProperties { get; private set; } public List Items { get; private set; } public JsonSchemaNode AdditionalProperties { get; set; } @@ -42,6 +43,7 @@ namespace Newtonsoft.Json.Schema { Schemas = new ReadOnlyCollection(new []{ schema }); Properties = new Dictionary(); + PatternProperties = new Dictionary(); Items = new List(); Id = GetId(Schemas); @@ -51,6 +53,7 @@ namespace Newtonsoft.Json.Schema { Schemas = new ReadOnlyCollection(source.Schemas.Union(new[] { schema }).ToList()); Properties = new Dictionary(source.Properties); + PatternProperties = new Dictionary(source.PatternProperties); Items = new List(source.Items); AdditionalProperties = source.AdditionalProperties; diff --git a/Src/Newtonsoft.Json/Schema/JsonSchemaWriter.cs b/Src/Newtonsoft.Json/Schema/JsonSchemaWriter.cs index c561e5fb..eeaa0dbd 100644 --- a/Src/Newtonsoft.Json/Schema/JsonSchemaWriter.cs +++ b/Src/Newtonsoft.Json/Schema/JsonSchemaWriter.cs @@ -89,20 +89,13 @@ namespace Newtonsoft.Json.Schema ReferenceOrWriteSchema(schema.AdditionalProperties); } } - if (schema.Properties != null) - { - _writer.WritePropertyName(JsonSchemaConstants.PropertiesPropertyName); - _writer.WriteStartObject(); - foreach (KeyValuePair property in schema.Properties) - { - _writer.WritePropertyName(property.Key); - ReferenceOrWriteSchema(property.Value); - } - _writer.WriteEndObject(); - } + WriteSchemaDictionaryIfNotNull(_writer, JsonSchemaConstants.PropertiesPropertyName, schema.Properties); + WriteSchemaDictionaryIfNotNull(_writer, JsonSchemaConstants.PatternPropertiesPropertyName, schema.PatternProperties); WriteItems(schema); WritePropertyIfNotNull(_writer, JsonSchemaConstants.MinimumPropertyName, schema.Minimum); WritePropertyIfNotNull(_writer, JsonSchemaConstants.MaximumPropertyName, schema.Maximum); + WritePropertyIfNotNull(_writer, JsonSchemaConstants.ExclusiveMinimumPropertyName, schema.ExclusiveMinimum); + WritePropertyIfNotNull(_writer, JsonSchemaConstants.ExclusiveMaximumPropertyName, schema.ExclusiveMaximum); WritePropertyIfNotNull(_writer, JsonSchemaConstants.MinimumLengthPropertyName, schema.MinimumLength); WritePropertyIfNotNull(_writer, JsonSchemaConstants.MaximumLengthPropertyName, schema.MaximumLength); WritePropertyIfNotNull(_writer, JsonSchemaConstants.MinimumItemsPropertyName, schema.MinimumItems); @@ -153,6 +146,21 @@ namespace Newtonsoft.Json.Schema _writer.WriteEndObject(); } + private void WriteSchemaDictionaryIfNotNull(JsonWriter writer, string propertyName, IDictionary properties) + { + if (properties != null) + { + writer.WritePropertyName(propertyName); + writer.WriteStartObject(); + foreach (KeyValuePair property in properties) + { + writer.WritePropertyName(property.Key); + ReferenceOrWriteSchema(property.Value); + } + writer.WriteEndObject(); + } + } + private void WriteItems(JsonSchema schema) { if (CollectionUtils.IsNullOrEmpty(schema.Items))