2 // Copyright (c) 2007 James Newton-King
4 // Permission is hereby granted, free of charge, to any person
5 // obtaining a copy of this software and associated documentation
6 // files (the "Software"), to deal in the Software without
7 // restriction, including without limitation the rights to use,
8 // copy, modify, merge, publish, distribute, sublicense, and/or sell
9 // copies of the Software, and to permit persons to whom the
10 // Software is furnished to do so, subject to the following
13 // The above copyright notice and this permission notice shall be
14 // included in all copies or substantial portions of the Software.
16 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20 // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21 // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23 // OTHER DEALINGS IN THE SOFTWARE.
27 using System.Collections.Generic;
30 using Newtonsoft.Json.Linq;
31 using Newtonsoft.Json.Schema;
32 using Newtonsoft.Json.Utilities;
33 using System.Globalization;
34 using System.Text.RegularExpressions;
37 namespace Newtonsoft.Json
40 /// Represents a reader that provides <see cref="JsonSchema"/> validation.
42 public class JsonValidatingReader : JsonReader, IJsonLineInfo
44 private class SchemaScope
46 private readonly JTokenType _tokenType;
47 private readonly IList<JsonSchemaModel> _schemas;
48 private readonly Dictionary<string, bool> _requiredProperties;
50 public string CurrentPropertyName { get; set; }
51 public int ArrayItemCount { get; set; }
53 public IList<JsonSchemaModel> Schemas
55 get { return _schemas; }
58 public Dictionary<string, bool> RequiredProperties
60 get { return _requiredProperties; }
63 public JTokenType TokenType
65 get { return _tokenType; }
68 public SchemaScope(JTokenType tokenType, IList<JsonSchemaModel> schemas)
70 _tokenType = tokenType;
73 _requiredProperties = schemas.SelectMany<JsonSchemaModel, string>(GetRequiredProperties).Distinct().ToDictionary(p => p, p => false);
76 private IEnumerable<string> GetRequiredProperties(JsonSchemaModel schema)
78 if (schema == null || schema.Properties == null)
79 return Enumerable.Empty<string>();
81 return schema.Properties.Where(p => p.Value.Required).Select(p => p.Key);
85 private readonly JsonReader _reader;
86 private readonly Stack<SchemaScope> _stack;
87 private JsonSchema _schema;
88 private JsonSchemaModel _model;
89 private SchemaScope _currentScope;
92 /// Sets an event handler for receiving schema validation errors.
94 public event ValidationEventHandler ValidationEventHandler;
97 /// Gets the text value of the current Json token.
100 public override object Value
102 get { return _reader.Value; }
106 /// Gets the depth of the current token in the JSON document.
108 /// <value>The depth of the current token in the JSON document.</value>
109 public override int Depth
111 get { return _reader.Depth; }
115 /// Gets the quotation mark character used to enclose the value of a string.
118 public override char QuoteChar
120 get { return _reader.QuoteChar; }
121 protected internal set { }
125 /// Gets the type of the current Json token.
128 public override JsonToken TokenType
130 get { return _reader.TokenType; }
134 /// Gets The Common Language Runtime (CLR) type for the current Json token.
137 public override Type ValueType
139 get { return _reader.ValueType; }
142 private void Push(SchemaScope scope)
145 _currentScope = scope;
148 private SchemaScope Pop()
150 SchemaScope poppedScope = _stack.Pop();
151 _currentScope = (_stack.Count != 0)
158 private IEnumerable<JsonSchemaModel> CurrentSchemas
160 get { return _currentScope.Schemas; }
163 private IEnumerable<JsonSchemaModel> CurrentMemberSchemas
167 if (_currentScope == null)
168 return new List<JsonSchemaModel>(new [] { _model });
170 if (_currentScope.Schemas == null || _currentScope.Schemas.Count == 0)
171 return Enumerable.Empty<JsonSchemaModel>();
173 switch (_currentScope.TokenType)
175 case JTokenType.None:
176 return _currentScope.Schemas;
177 case JTokenType.Object:
179 if (_currentScope.CurrentPropertyName == null)
180 throw new Exception("CurrentPropertyName has not been set on scope.");
182 IList<JsonSchemaModel> schemas = new List<JsonSchemaModel>();
184 foreach (JsonSchemaModel schema in CurrentSchemas)
186 JsonSchemaModel propertySchema;
187 if (schema.Properties != null && schema.Properties.TryGetValue(_currentScope.CurrentPropertyName, out propertySchema))
189 schemas.Add(propertySchema);
191 if (schema.PatternProperties != null)
193 foreach (KeyValuePair<string, JsonSchemaModel> patternProperty in schema.PatternProperties)
195 if (Regex.IsMatch(_currentScope.CurrentPropertyName, patternProperty.Key))
197 schemas.Add(patternProperty.Value);
202 if (schemas.Count == 0 && schema.AllowAdditionalProperties && schema.AdditionalProperties != null)
203 schemas.Add(schema.AdditionalProperties);
208 case JTokenType.Array:
210 IList<JsonSchemaModel> schemas = new List<JsonSchemaModel>();
212 foreach (JsonSchemaModel schema in CurrentSchemas)
214 if (!CollectionUtils.IsNullOrEmpty(schema.Items))
216 if (schema.Items.Count == 1)
217 schemas.Add(schema.Items[0]);
219 if (schema.Items.Count > (_currentScope.ArrayItemCount - 1))
220 schemas.Add(schema.Items[_currentScope.ArrayItemCount - 1]);
223 if (schema.AllowAdditionalProperties && schema.AdditionalProperties != null)
224 schemas.Add(schema.AdditionalProperties);
229 case JTokenType.Constructor:
230 return Enumerable.Empty<JsonSchemaModel>();
232 throw new ArgumentOutOfRangeException("TokenType", "Unexpected token type: {0}".FormatWith(CultureInfo.InvariantCulture, _currentScope.TokenType));
237 private void RaiseError(string message, JsonSchemaModel schema)
239 IJsonLineInfo lineInfo = this;
241 string exceptionMessage = (lineInfo.HasLineInfo())
242 ? message + " Line {0}, position {1}.".FormatWith(CultureInfo.InvariantCulture, lineInfo.LineNumber, lineInfo.LinePosition)
245 OnValidationEvent(new JsonSchemaException(exceptionMessage, null, lineInfo.LineNumber, lineInfo.LinePosition));
248 private void OnValidationEvent(JsonSchemaException exception)
250 ValidationEventHandler handler = ValidationEventHandler;
252 handler(this, new ValidationEventArgs(exception));
258 /// Initializes a new instance of the <see cref="JsonValidatingReader"/> class that
259 /// validates the content returned from the given <see cref="JsonReader"/>.
261 /// <param name="reader">The <see cref="JsonReader"/> to read from while validating.</param>
262 public JsonValidatingReader(JsonReader reader)
264 ValidationUtils.ArgumentNotNull(reader, "reader");
266 _stack = new Stack<SchemaScope>();
270 /// Gets or sets the schema.
272 /// <value>The schema.</value>
273 public JsonSchema Schema
275 get { return _schema; }
278 if (TokenType != JsonToken.None)
279 throw new Exception("Cannot change schema while validating JSON.");
287 /// Gets the <see cref="JsonReader"/> used to construct this <see cref="JsonValidatingReader"/>.
289 /// <value>The <see cref="JsonReader"/> specified in the constructor.</value>
290 public JsonReader Reader
292 get { return _reader; }
295 private void ValidateInEnumAndNotDisallowed(JsonSchemaModel schema)
300 JToken value = new JValue(_reader.Value);
302 if (schema.Enum != null)
304 StringWriter sw = new StringWriter(CultureInfo.InvariantCulture);
305 value.WriteTo(new JsonTextWriter(sw));
307 if (!schema.Enum.ContainsValue(value, new JTokenEqualityComparer()))
308 RaiseError("Value {0} is not defined in enum.".FormatWith(CultureInfo.InvariantCulture, sw.ToString()),
312 JsonSchemaType? currentNodeType = GetCurrentNodeSchemaType();
313 if (currentNodeType != null)
315 if (JsonSchemaGenerator.HasFlag(schema.Disallow, currentNodeType.Value))
316 RaiseError("Type {0} is disallowed.".FormatWith(CultureInfo.InvariantCulture, currentNodeType), schema);
320 private JsonSchemaType? GetCurrentNodeSchemaType()
322 switch (_reader.TokenType)
324 case JsonToken.StartObject:
325 return JsonSchemaType.Object;
326 case JsonToken.StartArray:
327 return JsonSchemaType.Array;
328 case JsonToken.Integer:
329 return JsonSchemaType.Integer;
330 case JsonToken.Float:
331 return JsonSchemaType.Float;
332 case JsonToken.String:
333 return JsonSchemaType.String;
334 case JsonToken.Boolean:
335 return JsonSchemaType.Boolean;
337 return JsonSchemaType.Null;
344 /// Reads the next JSON token from the stream as a <see cref="T:Byte[]"/>.
347 /// A <see cref="T:Byte[]"/> or a null reference if the next JSON token is null.
349 public override byte[] ReadAsBytes()
351 byte[] data = _reader.ReadAsBytes();
353 ValidateCurrentToken();
358 /// Reads the next JSON token from the stream as a <see cref="Nullable{Decimal}"/>.
360 /// <returns>A <see cref="Nullable{Decimal}"/>.</returns>
361 public override decimal? ReadAsDecimal()
363 decimal? d = _reader.ReadAsDecimal();
365 ValidateCurrentToken();
371 /// Reads the next JSON token from the stream as a <see cref="Nullable{DateTimeOffset}"/>.
373 /// <returns>A <see cref="Nullable{DateTimeOffset}"/>.</returns>
374 public override DateTimeOffset? ReadAsDateTimeOffset()
376 DateTimeOffset? dateTimeOffset = _reader.ReadAsDateTimeOffset();
378 ValidateCurrentToken();
379 return dateTimeOffset;
384 /// Reads the next JSON token from the stream.
387 /// true if the next token was read successfully; false if there are no more tokens to read.
389 public override bool Read()
394 if (_reader.TokenType == JsonToken.Comment)
397 ValidateCurrentToken();
401 private void ValidateCurrentToken()
403 // first time validate has been called. build model
406 JsonSchemaModelBuilder builder = new JsonSchemaModelBuilder();
407 _model = builder.Build(_schema);
410 //ValidateValueToken();
412 switch (_reader.TokenType)
414 case JsonToken.StartObject:
416 IList<JsonSchemaModel> objectSchemas = CurrentMemberSchemas.Where(ValidateObject).ToList();
417 Push(new SchemaScope(JTokenType.Object, objectSchemas));
419 case JsonToken.StartArray:
421 IList<JsonSchemaModel> arraySchemas = CurrentMemberSchemas.Where(ValidateArray).ToList();
422 Push(new SchemaScope(JTokenType.Array, arraySchemas));
424 case JsonToken.StartConstructor:
425 Push(new SchemaScope(JTokenType.Constructor, null));
427 case JsonToken.PropertyName:
428 foreach (JsonSchemaModel schema in CurrentSchemas)
430 ValidatePropertyName(schema);
435 case JsonToken.Integer:
437 foreach (JsonSchemaModel schema in CurrentMemberSchemas)
439 ValidateInteger(schema);
442 case JsonToken.Float:
444 foreach (JsonSchemaModel schema in CurrentMemberSchemas)
446 ValidateFloat(schema);
449 case JsonToken.String:
451 foreach (JsonSchemaModel schema in CurrentMemberSchemas)
453 ValidateString(schema);
456 case JsonToken.Boolean:
458 foreach (JsonSchemaModel schema in CurrentMemberSchemas)
460 ValidateBoolean(schema);
465 foreach (JsonSchemaModel schema in CurrentMemberSchemas)
467 ValidateNull(schema);
470 case JsonToken.Undefined:
472 case JsonToken.EndObject:
473 foreach (JsonSchemaModel schema in CurrentSchemas)
475 ValidateEndObject(schema);
479 case JsonToken.EndArray:
480 foreach (JsonSchemaModel schema in CurrentSchemas)
482 ValidateEndArray(schema);
486 case JsonToken.EndConstructor:
492 throw new ArgumentOutOfRangeException();
496 private void ValidateEndObject(JsonSchemaModel schema)
501 Dictionary<string, bool> requiredProperties = _currentScope.RequiredProperties;
503 if (requiredProperties != null)
505 List<string> unmatchedRequiredProperties =
506 requiredProperties.Where(kv => !kv.Value).Select(kv => kv.Key).ToList();
508 if (unmatchedRequiredProperties.Count > 0)
509 RaiseError("Required properties are missing from object: {0}.".FormatWith(CultureInfo.InvariantCulture, string.Join(", ", unmatchedRequiredProperties.ToArray())), schema);
513 private void ValidateEndArray(JsonSchemaModel schema)
518 int arrayItemCount = _currentScope.ArrayItemCount;
520 if (schema.MaximumItems != null && arrayItemCount > schema.MaximumItems)
521 RaiseError("Array item count {0} exceeds maximum count of {1}.".FormatWith(CultureInfo.InvariantCulture, arrayItemCount, schema.MaximumItems), schema);
523 if (schema.MinimumItems != null && arrayItemCount < schema.MinimumItems)
524 RaiseError("Array item count {0} is less than minimum count of {1}.".FormatWith(CultureInfo.InvariantCulture, arrayItemCount, schema.MinimumItems), schema);
527 private void ValidateNull(JsonSchemaModel schema)
532 if (!TestType(schema, JsonSchemaType.Null))
535 ValidateInEnumAndNotDisallowed(schema);
538 private void ValidateBoolean(JsonSchemaModel schema)
543 if (!TestType(schema, JsonSchemaType.Boolean))
546 ValidateInEnumAndNotDisallowed(schema);
549 private void ValidateString(JsonSchemaModel schema)
554 if (!TestType(schema, JsonSchemaType.String))
557 ValidateInEnumAndNotDisallowed(schema);
559 string value = _reader.Value.ToString();
561 if (schema.MaximumLength != null && value.Length > schema.MaximumLength)
562 RaiseError("String '{0}' exceeds maximum length of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.MaximumLength), schema);
564 if (schema.MinimumLength != null && value.Length < schema.MinimumLength)
565 RaiseError("String '{0}' is less than minimum length of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.MinimumLength), schema);
567 if (schema.Patterns != null)
569 foreach (string pattern in schema.Patterns)
571 if (!Regex.IsMatch(value, pattern))
572 RaiseError("String '{0}' does not match regex pattern '{1}'.".FormatWith(CultureInfo.InvariantCulture, value, pattern), schema);
577 private void ValidateInteger(JsonSchemaModel schema)
582 if (!TestType(schema, JsonSchemaType.Integer))
585 ValidateInEnumAndNotDisallowed(schema);
587 long value = Convert.ToInt64(_reader.Value, CultureInfo.InvariantCulture);
589 if (schema.Maximum != null)
591 if (value > schema.Maximum)
592 RaiseError("Integer {0} exceeds maximum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Maximum), schema);
593 if (schema.ExclusiveMaximum && value == schema.Maximum)
594 RaiseError("Integer {0} equals maximum value of {1} and exclusive maximum is true.".FormatWith(CultureInfo.InvariantCulture, value, schema.Maximum), schema);
597 if (schema.Minimum != null)
599 if (value < schema.Minimum)
600 RaiseError("Integer {0} is less than minimum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Minimum), schema);
601 if (schema.ExclusiveMinimum && value == schema.Minimum)
602 RaiseError("Integer {0} equals minimum value of {1} and exclusive minimum is true.".FormatWith(CultureInfo.InvariantCulture, value, schema.Minimum), schema);
605 if (schema.DivisibleBy != null && !IsZero(value % schema.DivisibleBy.Value))
606 RaiseError("Integer {0} is not evenly divisible by {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.DivisibleBy), schema);
609 private void ProcessValue()
611 if (_currentScope != null && _currentScope.TokenType == JTokenType.Array)
613 _currentScope.ArrayItemCount++;
615 foreach (JsonSchemaModel currentSchema in CurrentSchemas)
617 if (currentSchema != null && currentSchema.Items != null && currentSchema.Items.Count > 1 && _currentScope.ArrayItemCount >= currentSchema.Items.Count)
618 RaiseError("Index {0} has not been defined and the schema does not allow additional items.".FormatWith(CultureInfo.InvariantCulture, _currentScope.ArrayItemCount), currentSchema);
623 private void ValidateFloat(JsonSchemaModel schema)
628 if (!TestType(schema, JsonSchemaType.Float))
631 ValidateInEnumAndNotDisallowed(schema);
633 double value = Convert.ToDouble(_reader.Value, CultureInfo.InvariantCulture);
635 if (schema.Maximum != null)
637 if (value > schema.Maximum)
638 RaiseError("Float {0} exceeds maximum value of {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Maximum), schema);
639 if (schema.ExclusiveMaximum && value == schema.Maximum)
640 RaiseError("Float {0} equals maximum value of {1} and exclusive maximum is true.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Maximum), schema);
643 if (schema.Minimum != null)
645 if (value < schema.Minimum)
646 RaiseError("Float {0} is less than minimum value of {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Minimum), schema);
647 if (schema.ExclusiveMinimum && value == schema.Minimum)
648 RaiseError("Float {0} equals minimum value of {1} and exclusive minimum is true.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Minimum), schema);
651 if (schema.DivisibleBy != null && !IsZero(value % schema.DivisibleBy.Value))
652 RaiseError("Float {0} is not evenly divisible by {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.DivisibleBy), schema);
655 private static bool IsZero(double value)
657 double epsilon = 2.2204460492503131e-016;
659 return Math.Abs(value) < 10.0 * epsilon;
662 private void ValidatePropertyName(JsonSchemaModel schema)
667 string propertyName = Convert.ToString(_reader.Value, CultureInfo.InvariantCulture);
669 if (_currentScope.RequiredProperties.ContainsKey(propertyName))
670 _currentScope.RequiredProperties[propertyName] = true;
672 if (!schema.AllowAdditionalProperties)
674 bool propertyDefinied = IsPropertyDefinied(schema, propertyName);
676 if (!propertyDefinied)
677 RaiseError("Property '{0}' has not been defined and the schema does not allow additional properties.".FormatWith(CultureInfo.InvariantCulture, propertyName), schema);
680 _currentScope.CurrentPropertyName = propertyName;
683 private bool IsPropertyDefinied(JsonSchemaModel schema, string propertyName)
685 if (schema.Properties != null && schema.Properties.ContainsKey(propertyName))
688 if (schema.PatternProperties != null)
690 foreach (string pattern in schema.PatternProperties.Keys)
692 if (Regex.IsMatch(propertyName, pattern))
700 private bool ValidateArray(JsonSchemaModel schema)
705 return (TestType(schema, JsonSchemaType.Array));
708 private bool ValidateObject(JsonSchemaModel schema)
713 return (TestType(schema, JsonSchemaType.Object));
716 private bool TestType(JsonSchemaModel currentSchema, JsonSchemaType currentType)
718 if (!JsonSchemaGenerator.HasFlag(currentSchema.Type, currentType))
720 RaiseError("Invalid type. Expected {0} but got {1}.".FormatWith(CultureInfo.InvariantCulture, currentSchema.Type, currentType), currentSchema);
727 bool IJsonLineInfo.HasLineInfo()
729 IJsonLineInfo lineInfo = _reader as IJsonLineInfo;
730 return (lineInfo != null) ? lineInfo.HasLineInfo() : false;
733 int IJsonLineInfo.LineNumber
737 IJsonLineInfo lineInfo = _reader as IJsonLineInfo;
738 return (lineInfo != null) ? lineInfo.LineNumber : 0;
742 int IJsonLineInfo.LinePosition
746 IJsonLineInfo lineInfo = _reader as IJsonLineInfo;
747 return (lineInfo != null) ? lineInfo.LinePosition : 0;