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;
28
using Newtonsoft.Json.Linq;
29
using Newtonsoft.Json.Schema;
30
using Newtonsoft.Json.Utilities;
31
using System.Globalization;
32
using System.Text.RegularExpressions;
35
using Newtonsoft.Json.Utilities.LinqBridge;
40
namespace Newtonsoft.Json
43
/// Represents a reader that provides <see cref="JsonSchema"/> validation.
45
public class JsonValidatingReader : JsonReader, IJsonLineInfo
47
private class SchemaScope
49
private readonly JTokenType _tokenType;
50
private readonly IList<JsonSchemaModel> _schemas;
51
private readonly Dictionary<string, bool> _requiredProperties;
53
public string CurrentPropertyName { get; set; }
54
public int ArrayItemCount { get; set; }
56
public IList<JsonSchemaModel> Schemas
58
get { return _schemas; }
61
public Dictionary<string, bool> RequiredProperties
63
get { return _requiredProperties; }
66
public JTokenType TokenType
68
get { return _tokenType; }
71
public SchemaScope(JTokenType tokenType, IList<JsonSchemaModel> schemas)
73
_tokenType = tokenType;
76
_requiredProperties = schemas.SelectMany<JsonSchemaModel, string>(GetRequiredProperties).Distinct().ToDictionary(p => p, p => false);
79
private IEnumerable<string> GetRequiredProperties(JsonSchemaModel schema)
81
if (schema == null || schema.Properties == null)
82
return Enumerable.Empty<string>();
84
return schema.Properties.Where(p => p.Value.Required).Select(p => p.Key);
88
private readonly JsonReader _reader;
89
private readonly Stack<SchemaScope> _stack;
90
private JsonSchema _schema;
91
private JsonSchemaModel _model;
92
private SchemaScope _currentScope;
95
/// Sets an event handler for receiving schema validation errors.
97
public event ValidationEventHandler ValidationEventHandler;
100
/// Gets the text value of the current Json token.
103
public override object Value
105
get { return _reader.Value; }
109
/// Gets the depth of the current token in the JSON document.
111
/// <value>The depth of the current token in the JSON document.</value>
112
public override int Depth
114
get { return _reader.Depth; }
118
/// Gets the path of the current JSON token.
120
public override string Path
122
get { return _reader.Path; }
126
/// Gets the quotation mark character used to enclose the value of a string.
129
public override char QuoteChar
131
get { return _reader.QuoteChar; }
132
protected internal set { }
136
/// Gets the type of the current Json token.
139
public override JsonToken TokenType
141
get { return _reader.TokenType; }
145
/// Gets the Common Language Runtime (CLR) type for the current Json token.
148
public override Type ValueType
150
get { return _reader.ValueType; }
153
private void Push(SchemaScope scope)
156
_currentScope = scope;
159
private SchemaScope Pop()
161
SchemaScope poppedScope = _stack.Pop();
162
_currentScope = (_stack.Count != 0)
169
private IEnumerable<JsonSchemaModel> CurrentSchemas
171
get { return _currentScope.Schemas; }
174
private IEnumerable<JsonSchemaModel> CurrentMemberSchemas
178
if (_currentScope == null)
179
return new List<JsonSchemaModel>(new [] { _model });
181
if (_currentScope.Schemas == null || _currentScope.Schemas.Count == 0)
182
return Enumerable.Empty<JsonSchemaModel>();
184
switch (_currentScope.TokenType)
186
case JTokenType.None:
187
return _currentScope.Schemas;
188
case JTokenType.Object:
190
if (_currentScope.CurrentPropertyName == null)
191
throw new JsonReaderException("CurrentPropertyName has not been set on scope.");
193
IList<JsonSchemaModel> schemas = new List<JsonSchemaModel>();
195
foreach (JsonSchemaModel schema in CurrentSchemas)
197
JsonSchemaModel propertySchema;
198
if (schema.Properties != null && schema.Properties.TryGetValue(_currentScope.CurrentPropertyName, out propertySchema))
200
schemas.Add(propertySchema);
202
if (schema.PatternProperties != null)
204
foreach (KeyValuePair<string, JsonSchemaModel> patternProperty in schema.PatternProperties)
206
if (Regex.IsMatch(_currentScope.CurrentPropertyName, patternProperty.Key))
208
schemas.Add(patternProperty.Value);
213
if (schemas.Count == 0 && schema.AllowAdditionalProperties && schema.AdditionalProperties != null)
214
schemas.Add(schema.AdditionalProperties);
219
case JTokenType.Array:
221
IList<JsonSchemaModel> schemas = new List<JsonSchemaModel>();
223
foreach (JsonSchemaModel schema in CurrentSchemas)
225
if (!CollectionUtils.IsNullOrEmpty(schema.Items))
227
if (schema.Items.Count == 1)
229
schemas.Add(schema.Items[0]);
233
if (schema.Items.Count > (_currentScope.ArrayItemCount - 1))
234
schemas.Add(schema.Items[_currentScope.ArrayItemCount - 1]);
238
if (schema.AllowAdditionalProperties && schema.AdditionalProperties != null)
239
schemas.Add(schema.AdditionalProperties);
244
case JTokenType.Constructor:
245
return Enumerable.Empty<JsonSchemaModel>();
247
throw new ArgumentOutOfRangeException("TokenType", "Unexpected token type: {0}".FormatWith(CultureInfo.InvariantCulture, _currentScope.TokenType));
252
private void RaiseError(string message, JsonSchemaModel schema)
254
IJsonLineInfo lineInfo = this;
256
string exceptionMessage = (lineInfo.HasLineInfo())
257
? message + " Line {0}, position {1}.".FormatWith(CultureInfo.InvariantCulture, lineInfo.LineNumber, lineInfo.LinePosition)
260
OnValidationEvent(new JsonSchemaException(exceptionMessage, null, Path, lineInfo.LineNumber, lineInfo.LinePosition));
263
private void OnValidationEvent(JsonSchemaException exception)
265
ValidationEventHandler handler = ValidationEventHandler;
267
handler(this, new ValidationEventArgs(exception));
273
/// Initializes a new instance of the <see cref="JsonValidatingReader"/> class that
274
/// validates the content returned from the given <see cref="JsonReader"/>.
276
/// <param name="reader">The <see cref="JsonReader"/> to read from while validating.</param>
277
public JsonValidatingReader(JsonReader reader)
279
ValidationUtils.ArgumentNotNull(reader, "reader");
281
_stack = new Stack<SchemaScope>();
285
/// Gets or sets the schema.
287
/// <value>The schema.</value>
288
public JsonSchema Schema
290
get { return _schema; }
293
if (TokenType != JsonToken.None)
294
throw new InvalidOperationException("Cannot change schema while validating JSON.");
302
/// Gets the <see cref="JsonReader"/> used to construct this <see cref="JsonValidatingReader"/>.
304
/// <value>The <see cref="JsonReader"/> specified in the constructor.</value>
305
public JsonReader Reader
307
get { return _reader; }
310
private void ValidateInEnumAndNotDisallowed(JsonSchemaModel schema)
315
JToken value = new JValue(_reader.Value);
317
if (schema.Enum != null)
319
StringWriter sw = new StringWriter(CultureInfo.InvariantCulture);
320
value.WriteTo(new JsonTextWriter(sw));
322
if (!schema.Enum.ContainsValue(value, new JTokenEqualityComparer()))
323
RaiseError("Value {0} is not defined in enum.".FormatWith(CultureInfo.InvariantCulture, sw.ToString()),
327
JsonSchemaType? currentNodeType = GetCurrentNodeSchemaType();
328
if (currentNodeType != null)
330
if (JsonSchemaGenerator.HasFlag(schema.Disallow, currentNodeType.Value))
331
RaiseError("Type {0} is disallowed.".FormatWith(CultureInfo.InvariantCulture, currentNodeType), schema);
335
private JsonSchemaType? GetCurrentNodeSchemaType()
337
switch (_reader.TokenType)
339
case JsonToken.StartObject:
340
return JsonSchemaType.Object;
341
case JsonToken.StartArray:
342
return JsonSchemaType.Array;
343
case JsonToken.Integer:
344
return JsonSchemaType.Integer;
345
case JsonToken.Float:
346
return JsonSchemaType.Float;
347
case JsonToken.String:
348
return JsonSchemaType.String;
349
case JsonToken.Boolean:
350
return JsonSchemaType.Boolean;
352
return JsonSchemaType.Null;
359
/// Reads the next JSON token from the stream as a <see cref="Nullable{Int32}"/>.
361
/// <returns>A <see cref="Nullable{Int32}"/>.</returns>
362
public override int? ReadAsInt32()
364
int? i = _reader.ReadAsInt32();
366
ValidateCurrentToken();
371
/// Reads the next JSON token from the stream as a <see cref="T:Byte[]"/>.
374
/// A <see cref="T:Byte[]"/> or a null reference if the next JSON token is null.
376
public override byte[] ReadAsBytes()
378
byte[] data = _reader.ReadAsBytes();
380
ValidateCurrentToken();
385
/// Reads the next JSON token from the stream as a <see cref="Nullable{Decimal}"/>.
387
/// <returns>A <see cref="Nullable{Decimal}"/>.</returns>
388
public override decimal? ReadAsDecimal()
390
decimal? d = _reader.ReadAsDecimal();
392
ValidateCurrentToken();
397
/// Reads the next JSON token from the stream as a <see cref="String"/>.
399
/// <returns>A <see cref="String"/>. This method will return <c>null</c> at the end of an array.</returns>
400
public override string ReadAsString()
402
string s = _reader.ReadAsString();
404
ValidateCurrentToken();
409
/// Reads the next JSON token from the stream as a <see cref="Nullable{DateTime}"/>.
411
/// <returns>A <see cref="String"/>. This method will return <c>null</c> at the end of an array.</returns>
412
public override DateTime? ReadAsDateTime()
414
DateTime? dateTime = _reader.ReadAsDateTime();
416
ValidateCurrentToken();
422
/// Reads the next JSON token from the stream as a <see cref="Nullable{DateTimeOffset}"/>.
424
/// <returns>A <see cref="Nullable{DateTimeOffset}"/>.</returns>
425
public override DateTimeOffset? ReadAsDateTimeOffset()
427
DateTimeOffset? dateTimeOffset = _reader.ReadAsDateTimeOffset();
429
ValidateCurrentToken();
430
return dateTimeOffset;
435
/// Reads the next JSON token from the stream.
438
/// true if the next token was read successfully; false if there are no more tokens to read.
440
public override bool Read()
445
if (_reader.TokenType == JsonToken.Comment)
448
ValidateCurrentToken();
452
private void ValidateCurrentToken()
454
// first time validate has been called. build model
457
JsonSchemaModelBuilder builder = new JsonSchemaModelBuilder();
458
_model = builder.Build(_schema);
461
switch (_reader.TokenType)
463
case JsonToken.StartObject:
465
IList<JsonSchemaModel> objectSchemas = CurrentMemberSchemas.Where(ValidateObject).ToList();
466
Push(new SchemaScope(JTokenType.Object, objectSchemas));
468
case JsonToken.StartArray:
470
IList<JsonSchemaModel> arraySchemas = CurrentMemberSchemas.Where(ValidateArray).ToList();
471
Push(new SchemaScope(JTokenType.Array, arraySchemas));
473
case JsonToken.StartConstructor:
474
Push(new SchemaScope(JTokenType.Constructor, null));
476
case JsonToken.PropertyName:
477
foreach (JsonSchemaModel schema in CurrentSchemas)
479
ValidatePropertyName(schema);
484
case JsonToken.Integer:
486
foreach (JsonSchemaModel schema in CurrentMemberSchemas)
488
ValidateInteger(schema);
491
case JsonToken.Float:
493
foreach (JsonSchemaModel schema in CurrentMemberSchemas)
495
ValidateFloat(schema);
498
case JsonToken.String:
500
foreach (JsonSchemaModel schema in CurrentMemberSchemas)
502
ValidateString(schema);
505
case JsonToken.Boolean:
507
foreach (JsonSchemaModel schema in CurrentMemberSchemas)
509
ValidateBoolean(schema);
514
foreach (JsonSchemaModel schema in CurrentMemberSchemas)
516
ValidateNull(schema);
519
case JsonToken.Undefined:
521
case JsonToken.EndObject:
522
foreach (JsonSchemaModel schema in CurrentSchemas)
524
ValidateEndObject(schema);
528
case JsonToken.EndArray:
529
foreach (JsonSchemaModel schema in CurrentSchemas)
531
ValidateEndArray(schema);
535
case JsonToken.EndConstructor:
539
case JsonToken.Bytes:
540
// these have no equivalent in JSON schema
543
// no content, do nothing
546
throw new ArgumentOutOfRangeException();
550
private void ValidateEndObject(JsonSchemaModel schema)
555
Dictionary<string, bool> requiredProperties = _currentScope.RequiredProperties;
557
if (requiredProperties != null)
559
List<string> unmatchedRequiredProperties =
560
requiredProperties.Where(kv => !kv.Value).Select(kv => kv.Key).ToList();
562
if (unmatchedRequiredProperties.Count > 0)
563
RaiseError("Required properties are missing from object: {0}.".FormatWith(CultureInfo.InvariantCulture, string.Join(", ", unmatchedRequiredProperties.ToArray())), schema);
567
private void ValidateEndArray(JsonSchemaModel schema)
572
int arrayItemCount = _currentScope.ArrayItemCount;
574
if (schema.MaximumItems != null && arrayItemCount > schema.MaximumItems)
575
RaiseError("Array item count {0} exceeds maximum count of {1}.".FormatWith(CultureInfo.InvariantCulture, arrayItemCount, schema.MaximumItems), schema);
577
if (schema.MinimumItems != null && arrayItemCount < schema.MinimumItems)
578
RaiseError("Array item count {0} is less than minimum count of {1}.".FormatWith(CultureInfo.InvariantCulture, arrayItemCount, schema.MinimumItems), schema);
581
private void ValidateNull(JsonSchemaModel schema)
586
if (!TestType(schema, JsonSchemaType.Null))
589
ValidateInEnumAndNotDisallowed(schema);
592
private void ValidateBoolean(JsonSchemaModel schema)
597
if (!TestType(schema, JsonSchemaType.Boolean))
600
ValidateInEnumAndNotDisallowed(schema);
603
private void ValidateString(JsonSchemaModel schema)
608
if (!TestType(schema, JsonSchemaType.String))
611
ValidateInEnumAndNotDisallowed(schema);
613
string value = _reader.Value.ToString();
615
if (schema.MaximumLength != null && value.Length > schema.MaximumLength)
616
RaiseError("String '{0}' exceeds maximum length of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.MaximumLength), schema);
618
if (schema.MinimumLength != null && value.Length < schema.MinimumLength)
619
RaiseError("String '{0}' is less than minimum length of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.MinimumLength), schema);
621
if (schema.Patterns != null)
623
foreach (string pattern in schema.Patterns)
625
if (!Regex.IsMatch(value, pattern))
626
RaiseError("String '{0}' does not match regex pattern '{1}'.".FormatWith(CultureInfo.InvariantCulture, value, pattern), schema);
631
private void ValidateInteger(JsonSchemaModel schema)
636
if (!TestType(schema, JsonSchemaType.Integer))
639
ValidateInEnumAndNotDisallowed(schema);
641
long value = Convert.ToInt64(_reader.Value, CultureInfo.InvariantCulture);
643
if (schema.Maximum != null)
645
if (value > schema.Maximum)
646
RaiseError("Integer {0} exceeds maximum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Maximum), schema);
647
if (schema.ExclusiveMaximum && value == schema.Maximum)
648
RaiseError("Integer {0} equals maximum value of {1} and exclusive maximum is true.".FormatWith(CultureInfo.InvariantCulture, value, schema.Maximum), schema);
651
if (schema.Minimum != null)
653
if (value < schema.Minimum)
654
RaiseError("Integer {0} is less than minimum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Minimum), schema);
655
if (schema.ExclusiveMinimum && value == schema.Minimum)
656
RaiseError("Integer {0} equals minimum value of {1} and exclusive minimum is true.".FormatWith(CultureInfo.InvariantCulture, value, schema.Minimum), schema);
659
if (schema.DivisibleBy != null && !IsZero(value % schema.DivisibleBy.Value))
660
RaiseError("Integer {0} is not evenly divisible by {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.DivisibleBy), schema);
663
private void ProcessValue()
665
if (_currentScope != null && _currentScope.TokenType == JTokenType.Array)
667
_currentScope.ArrayItemCount++;
669
foreach (JsonSchemaModel currentSchema in CurrentSchemas)
671
if (currentSchema != null && currentSchema.Items != null && currentSchema.Items.Count > 1 && _currentScope.ArrayItemCount >= currentSchema.Items.Count)
672
RaiseError("Index {0} has not been defined and the schema does not allow additional items.".FormatWith(CultureInfo.InvariantCulture, _currentScope.ArrayItemCount), currentSchema);
677
private void ValidateFloat(JsonSchemaModel schema)
682
if (!TestType(schema, JsonSchemaType.Float))
685
ValidateInEnumAndNotDisallowed(schema);
687
double value = Convert.ToDouble(_reader.Value, CultureInfo.InvariantCulture);
689
if (schema.Maximum != null)
691
if (value > schema.Maximum)
692
RaiseError("Float {0} exceeds maximum value of {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Maximum), schema);
693
if (schema.ExclusiveMaximum && value == schema.Maximum)
694
RaiseError("Float {0} equals maximum value of {1} and exclusive maximum is true.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Maximum), schema);
697
if (schema.Minimum != null)
699
if (value < schema.Minimum)
700
RaiseError("Float {0} is less than minimum value of {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Minimum), schema);
701
if (schema.ExclusiveMinimum && value == schema.Minimum)
702
RaiseError("Float {0} equals minimum value of {1} and exclusive minimum is true.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Minimum), schema);
705
if (schema.DivisibleBy != null && !IsZero(value % schema.DivisibleBy.Value))
706
RaiseError("Float {0} is not evenly divisible by {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.DivisibleBy), schema);
709
private static bool IsZero(double value)
711
const double epsilon = 2.2204460492503131e-016;
713
return Math.Abs(value) < 10.0 * epsilon;
716
private void ValidatePropertyName(JsonSchemaModel schema)
721
string propertyName = Convert.ToString(_reader.Value, CultureInfo.InvariantCulture);
723
if (_currentScope.RequiredProperties.ContainsKey(propertyName))
724
_currentScope.RequiredProperties[propertyName] = true;
726
if (!schema.AllowAdditionalProperties)
728
bool propertyDefinied = IsPropertyDefinied(schema, propertyName);
730
if (!propertyDefinied)
731
RaiseError("Property '{0}' has not been defined and the schema does not allow additional properties.".FormatWith(CultureInfo.InvariantCulture, propertyName), schema);
734
_currentScope.CurrentPropertyName = propertyName;
737
private bool IsPropertyDefinied(JsonSchemaModel schema, string propertyName)
739
if (schema.Properties != null && schema.Properties.ContainsKey(propertyName))
742
if (schema.PatternProperties != null)
744
foreach (string pattern in schema.PatternProperties.Keys)
746
if (Regex.IsMatch(propertyName, pattern))
754
private bool ValidateArray(JsonSchemaModel schema)
759
return (TestType(schema, JsonSchemaType.Array));
762
private bool ValidateObject(JsonSchemaModel schema)
767
return (TestType(schema, JsonSchemaType.Object));
770
private bool TestType(JsonSchemaModel currentSchema, JsonSchemaType currentType)
772
if (!JsonSchemaGenerator.HasFlag(currentSchema.Type, currentType))
774
RaiseError("Invalid type. Expected {0} but got {1}.".FormatWith(CultureInfo.InvariantCulture, currentSchema.Type, currentType), currentSchema);
781
bool IJsonLineInfo.HasLineInfo()
783
IJsonLineInfo lineInfo = _reader as IJsonLineInfo;
784
return lineInfo != null && lineInfo.HasLineInfo();
787
int IJsonLineInfo.LineNumber
791
IJsonLineInfo lineInfo = _reader as IJsonLineInfo;
792
return (lineInfo != null) ? lineInfo.LineNumber : 0;
796
int IJsonLineInfo.LinePosition
800
IJsonLineInfo lineInfo = _reader as IJsonLineInfo;
801
return (lineInfo != null) ? lineInfo.LinePosition : 0;
b'\\ No newline at end of file'