1023 lines
38 KiB
C#
1023 lines
38 KiB
C#
|
#region License
|
||
|
// Copyright (c) 2007 James Newton-King
|
||
|
//
|
||
|
// Permission is hereby granted, free of charge, to any person
|
||
|
// obtaining a copy of this software and associated documentation
|
||
|
// files (the "Software"), to deal in the Software without
|
||
|
// restriction, including without limitation the rights to use,
|
||
|
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
// copies of the Software, and to permit persons to whom the
|
||
|
// Software is furnished to do so, subject to the following
|
||
|
// conditions:
|
||
|
//
|
||
|
// The above copyright notice and this permission notice shall be
|
||
|
// included in all copies or substantial portions of the Software.
|
||
|
//
|
||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||
|
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||
|
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||
|
#endregion
|
||
|
|
||
|
using System;
|
||
|
using System.Collections.Generic;
|
||
|
#if HAVE_BIG_INTEGER
|
||
|
using System.Numerics;
|
||
|
#endif
|
||
|
using LC.Newtonsoft.Json.Linq;
|
||
|
using LC.Newtonsoft.Json.Schema;
|
||
|
using LC.Newtonsoft.Json.Utilities;
|
||
|
using System.Globalization;
|
||
|
using System.Text.RegularExpressions;
|
||
|
using System.IO;
|
||
|
#if !HAVE_LINQ
|
||
|
using LC.Newtonsoft.Json.Utilities.LinqBridge;
|
||
|
#else
|
||
|
using System.Linq;
|
||
|
|
||
|
#endif
|
||
|
|
||
|
namespace LC.Newtonsoft.Json
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// <para>
|
||
|
/// Represents a reader that provides <see cref="JsonSchema"/> validation.
|
||
|
/// </para>
|
||
|
/// <note type="caution">
|
||
|
/// JSON Schema validation has been moved to its own package. See <see href="https://www.newtonsoft.com/jsonschema">https://www.newtonsoft.com/jsonschema</see> for more details.
|
||
|
/// </note>
|
||
|
/// </summary>
|
||
|
[Obsolete("JSON Schema validation has been moved to its own package. See https://www.newtonsoft.com/jsonschema for more details.")]
|
||
|
public class JsonValidatingReader : JsonReader, IJsonLineInfo
|
||
|
{
|
||
|
private class SchemaScope
|
||
|
{
|
||
|
private readonly JTokenType _tokenType;
|
||
|
private readonly IList<JsonSchemaModel> _schemas;
|
||
|
private readonly Dictionary<string, bool> _requiredProperties;
|
||
|
|
||
|
public string CurrentPropertyName { get; set; }
|
||
|
public int ArrayItemCount { get; set; }
|
||
|
public bool IsUniqueArray { get; }
|
||
|
public IList<JToken> UniqueArrayItems { get; }
|
||
|
public JTokenWriter CurrentItemWriter { get; set; }
|
||
|
|
||
|
public IList<JsonSchemaModel> Schemas => _schemas;
|
||
|
|
||
|
public Dictionary<string, bool> RequiredProperties => _requiredProperties;
|
||
|
|
||
|
public JTokenType TokenType => _tokenType;
|
||
|
|
||
|
public SchemaScope(JTokenType tokenType, IList<JsonSchemaModel> schemas)
|
||
|
{
|
||
|
_tokenType = tokenType;
|
||
|
_schemas = schemas;
|
||
|
|
||
|
_requiredProperties = schemas.SelectMany<JsonSchemaModel, string>(GetRequiredProperties).Distinct().ToDictionary(p => p, p => false);
|
||
|
|
||
|
if (tokenType == JTokenType.Array && schemas.Any(s => s.UniqueItems))
|
||
|
{
|
||
|
IsUniqueArray = true;
|
||
|
UniqueArrayItems = new List<JToken>();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private IEnumerable<string> GetRequiredProperties(JsonSchemaModel schema)
|
||
|
{
|
||
|
if (schema?.Properties == null)
|
||
|
{
|
||
|
return Enumerable.Empty<string>();
|
||
|
}
|
||
|
|
||
|
return schema.Properties.Where(p => p.Value.Required).Select(p => p.Key);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private readonly JsonReader _reader;
|
||
|
private readonly Stack<SchemaScope> _stack;
|
||
|
private JsonSchema _schema;
|
||
|
private JsonSchemaModel _model;
|
||
|
private SchemaScope _currentScope;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Sets an event handler for receiving schema validation errors.
|
||
|
/// </summary>
|
||
|
public event ValidationEventHandler ValidationEventHandler;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Gets the text value of the current JSON token.
|
||
|
/// </summary>
|
||
|
/// <value></value>
|
||
|
public override object Value => _reader.Value;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Gets the depth of the current token in the JSON document.
|
||
|
/// </summary>
|
||
|
/// <value>The depth of the current token in the JSON document.</value>
|
||
|
public override int Depth => _reader.Depth;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Gets the path of the current JSON token.
|
||
|
/// </summary>
|
||
|
public override string Path => _reader.Path;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Gets the quotation mark character used to enclose the value of a string.
|
||
|
/// </summary>
|
||
|
/// <value></value>
|
||
|
public override char QuoteChar
|
||
|
{
|
||
|
get { return _reader.QuoteChar; }
|
||
|
protected internal set { }
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Gets the type of the current JSON token.
|
||
|
/// </summary>
|
||
|
/// <value></value>
|
||
|
public override JsonToken TokenType => _reader.TokenType;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Gets the .NET type for the current JSON token.
|
||
|
/// </summary>
|
||
|
/// <value></value>
|
||
|
public override Type ValueType => _reader.ValueType;
|
||
|
|
||
|
private void Push(SchemaScope scope)
|
||
|
{
|
||
|
_stack.Push(scope);
|
||
|
_currentScope = scope;
|
||
|
}
|
||
|
|
||
|
private SchemaScope Pop()
|
||
|
{
|
||
|
SchemaScope poppedScope = _stack.Pop();
|
||
|
_currentScope = (_stack.Count != 0)
|
||
|
? _stack.Peek()
|
||
|
: null;
|
||
|
|
||
|
return poppedScope;
|
||
|
}
|
||
|
|
||
|
private IList<JsonSchemaModel> CurrentSchemas => _currentScope.Schemas;
|
||
|
|
||
|
private static readonly IList<JsonSchemaModel> EmptySchemaList = new List<JsonSchemaModel>();
|
||
|
|
||
|
private IList<JsonSchemaModel> CurrentMemberSchemas
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
if (_currentScope == null)
|
||
|
{
|
||
|
return new List<JsonSchemaModel>(new[] { _model });
|
||
|
}
|
||
|
|
||
|
if (_currentScope.Schemas == null || _currentScope.Schemas.Count == 0)
|
||
|
{
|
||
|
return EmptySchemaList;
|
||
|
}
|
||
|
|
||
|
switch (_currentScope.TokenType)
|
||
|
{
|
||
|
case JTokenType.None:
|
||
|
return _currentScope.Schemas;
|
||
|
case JTokenType.Object:
|
||
|
{
|
||
|
if (_currentScope.CurrentPropertyName == null)
|
||
|
{
|
||
|
throw new JsonReaderException("CurrentPropertyName has not been set on scope.");
|
||
|
}
|
||
|
|
||
|
IList<JsonSchemaModel> schemas = new List<JsonSchemaModel>();
|
||
|
|
||
|
foreach (JsonSchemaModel schema in CurrentSchemas)
|
||
|
{
|
||
|
if (schema.Properties != null && schema.Properties.TryGetValue(_currentScope.CurrentPropertyName, out JsonSchemaModel propertySchema))
|
||
|
{
|
||
|
schemas.Add(propertySchema);
|
||
|
}
|
||
|
if (schema.PatternProperties != null)
|
||
|
{
|
||
|
foreach (KeyValuePair<string, JsonSchemaModel> 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<JsonSchemaModel> schemas = new List<JsonSchemaModel>();
|
||
|
|
||
|
foreach (JsonSchemaModel schema in CurrentSchemas)
|
||
|
{
|
||
|
if (!schema.PositionalItemsValidation)
|
||
|
{
|
||
|
if (schema.Items != null && schema.Items.Count > 0)
|
||
|
{
|
||
|
schemas.Add(schema.Items[0]);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if (schema.Items != null && schema.Items.Count > 0)
|
||
|
{
|
||
|
if (schema.Items.Count > (_currentScope.ArrayItemCount - 1))
|
||
|
{
|
||
|
schemas.Add(schema.Items[_currentScope.ArrayItemCount - 1]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (schema.AllowAdditionalItems && schema.AdditionalItems != null)
|
||
|
{
|
||
|
schemas.Add(schema.AdditionalItems);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return schemas;
|
||
|
}
|
||
|
case JTokenType.Constructor:
|
||
|
return EmptySchemaList;
|
||
|
default:
|
||
|
throw new ArgumentOutOfRangeException("TokenType", "Unexpected token type: {0}".FormatWith(CultureInfo.InvariantCulture, _currentScope.TokenType));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void RaiseError(string message, JsonSchemaModel schema)
|
||
|
{
|
||
|
IJsonLineInfo lineInfo = this;
|
||
|
|
||
|
string exceptionMessage = (lineInfo.HasLineInfo())
|
||
|
? message + " Line {0}, position {1}.".FormatWith(CultureInfo.InvariantCulture, lineInfo.LineNumber, lineInfo.LinePosition)
|
||
|
: message;
|
||
|
|
||
|
OnValidationEvent(new JsonSchemaException(exceptionMessage, null, Path, lineInfo.LineNumber, lineInfo.LinePosition));
|
||
|
}
|
||
|
|
||
|
private void OnValidationEvent(JsonSchemaException exception)
|
||
|
{
|
||
|
ValidationEventHandler handler = ValidationEventHandler;
|
||
|
if (handler != null)
|
||
|
{
|
||
|
handler(this, new ValidationEventArgs(exception));
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
throw exception;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Initializes a new instance of the <see cref="JsonValidatingReader"/> class that
|
||
|
/// validates the content returned from the given <see cref="JsonReader"/>.
|
||
|
/// </summary>
|
||
|
/// <param name="reader">The <see cref="JsonReader"/> to read from while validating.</param>
|
||
|
public JsonValidatingReader(JsonReader reader)
|
||
|
{
|
||
|
ValidationUtils.ArgumentNotNull(reader, nameof(reader));
|
||
|
_reader = reader;
|
||
|
_stack = new Stack<SchemaScope>();
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Gets or sets the schema.
|
||
|
/// </summary>
|
||
|
/// <value>The schema.</value>
|
||
|
public JsonSchema Schema
|
||
|
{
|
||
|
get => _schema;
|
||
|
set
|
||
|
{
|
||
|
if (TokenType != JsonToken.None)
|
||
|
{
|
||
|
throw new InvalidOperationException("Cannot change schema while validating JSON.");
|
||
|
}
|
||
|
|
||
|
_schema = value;
|
||
|
_model = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Gets the <see cref="JsonReader"/> used to construct this <see cref="JsonValidatingReader"/>.
|
||
|
/// </summary>
|
||
|
/// <value>The <see cref="JsonReader"/> specified in the constructor.</value>
|
||
|
public JsonReader Reader => _reader;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Changes the reader's state to <see cref="JsonReader.State.Closed"/>.
|
||
|
/// If <see cref="JsonReader.CloseInput"/> is set to <c>true</c>, the underlying <see cref="JsonReader"/> is also closed.
|
||
|
/// </summary>
|
||
|
public override void Close()
|
||
|
{
|
||
|
base.Close();
|
||
|
if (CloseInput)
|
||
|
{
|
||
|
_reader?.Close();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void ValidateNotDisallowed(JsonSchemaModel schema)
|
||
|
{
|
||
|
if (schema == null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
JsonSchemaType? currentNodeType = GetCurrentNodeSchemaType();
|
||
|
if (currentNodeType != null)
|
||
|
{
|
||
|
if (JsonSchemaGenerator.HasFlag(schema.Disallow, currentNodeType.GetValueOrDefault()))
|
||
|
{
|
||
|
RaiseError("Type {0} is disallowed.".FormatWith(CultureInfo.InvariantCulture, currentNodeType), schema);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private JsonSchemaType? GetCurrentNodeSchemaType()
|
||
|
{
|
||
|
switch (_reader.TokenType)
|
||
|
{
|
||
|
case JsonToken.StartObject:
|
||
|
return JsonSchemaType.Object;
|
||
|
case JsonToken.StartArray:
|
||
|
return JsonSchemaType.Array;
|
||
|
case JsonToken.Integer:
|
||
|
return JsonSchemaType.Integer;
|
||
|
case JsonToken.Float:
|
||
|
return JsonSchemaType.Float;
|
||
|
case JsonToken.String:
|
||
|
return JsonSchemaType.String;
|
||
|
case JsonToken.Boolean:
|
||
|
return JsonSchemaType.Boolean;
|
||
|
case JsonToken.Null:
|
||
|
return JsonSchemaType.Null;
|
||
|
default:
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="Nullable{T}"/> of <see cref="Int32"/>.
|
||
|
/// </summary>
|
||
|
/// <returns>A <see cref="Nullable{T}"/> of <see cref="Int32"/>.</returns>
|
||
|
public override int? ReadAsInt32()
|
||
|
{
|
||
|
int? i = _reader.ReadAsInt32();
|
||
|
|
||
|
ValidateCurrentToken();
|
||
|
return i;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="Byte"/>[].
|
||
|
/// </summary>
|
||
|
/// <returns>
|
||
|
/// A <see cref="Byte"/>[] or <c>null</c> if the next JSON token is null.
|
||
|
/// </returns>
|
||
|
public override byte[] ReadAsBytes()
|
||
|
{
|
||
|
byte[] data = _reader.ReadAsBytes();
|
||
|
|
||
|
ValidateCurrentToken();
|
||
|
return data;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="Nullable{T}"/> of <see cref="Decimal"/>.
|
||
|
/// </summary>
|
||
|
/// <returns>A <see cref="Nullable{T}"/> of <see cref="Decimal"/>.</returns>
|
||
|
public override decimal? ReadAsDecimal()
|
||
|
{
|
||
|
decimal? d = _reader.ReadAsDecimal();
|
||
|
|
||
|
ValidateCurrentToken();
|
||
|
return d;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="Nullable{T}"/> of <see cref="Double"/>.
|
||
|
/// </summary>
|
||
|
/// <returns>A <see cref="Nullable{T}"/> of <see cref="Double"/>.</returns>
|
||
|
public override double? ReadAsDouble()
|
||
|
{
|
||
|
double? d = _reader.ReadAsDouble();
|
||
|
|
||
|
ValidateCurrentToken();
|
||
|
return d;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="Nullable{T}"/> of <see cref="Boolean"/>.
|
||
|
/// </summary>
|
||
|
/// <returns>A <see cref="Nullable{T}"/> of <see cref="Boolean"/>.</returns>
|
||
|
public override bool? ReadAsBoolean()
|
||
|
{
|
||
|
bool? b = _reader.ReadAsBoolean();
|
||
|
|
||
|
ValidateCurrentToken();
|
||
|
return b;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="String"/>.
|
||
|
/// </summary>
|
||
|
/// <returns>A <see cref="String"/>. This method will return <c>null</c> at the end of an array.</returns>
|
||
|
public override string ReadAsString()
|
||
|
{
|
||
|
string s = _reader.ReadAsString();
|
||
|
|
||
|
ValidateCurrentToken();
|
||
|
return s;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="Nullable{T}"/> of <see cref="DateTime"/>.
|
||
|
/// </summary>
|
||
|
/// <returns>A <see cref="Nullable{T}"/> of <see cref="DateTime"/>. This method will return <c>null</c> at the end of an array.</returns>
|
||
|
public override DateTime? ReadAsDateTime()
|
||
|
{
|
||
|
DateTime? dateTime = _reader.ReadAsDateTime();
|
||
|
|
||
|
ValidateCurrentToken();
|
||
|
return dateTime;
|
||
|
}
|
||
|
|
||
|
#if HAVE_DATE_TIME_OFFSET
|
||
|
/// <summary>
|
||
|
/// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="Nullable{T}"/> of <see cref="DateTimeOffset"/>.
|
||
|
/// </summary>
|
||
|
/// <returns>A <see cref="Nullable{T}"/> of <see cref="DateTimeOffset"/>.</returns>
|
||
|
public override DateTimeOffset? ReadAsDateTimeOffset()
|
||
|
{
|
||
|
DateTimeOffset? dateTimeOffset = _reader.ReadAsDateTimeOffset();
|
||
|
|
||
|
ValidateCurrentToken();
|
||
|
return dateTimeOffset;
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
/// <summary>
|
||
|
/// Reads the next JSON token from the underlying <see cref="JsonReader"/>.
|
||
|
/// </summary>
|
||
|
/// <returns>
|
||
|
/// <c>true</c> if the next token was read successfully; <c>false</c> if there are no more tokens to read.
|
||
|
/// </returns>
|
||
|
public override bool Read()
|
||
|
{
|
||
|
if (!_reader.Read())
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (_reader.TokenType == JsonToken.Comment)
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
ValidateCurrentToken();
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private void ValidateCurrentToken()
|
||
|
{
|
||
|
// first time validate has been called. build model
|
||
|
if (_model == null)
|
||
|
{
|
||
|
JsonSchemaModelBuilder builder = new JsonSchemaModelBuilder();
|
||
|
_model = builder.Build(_schema);
|
||
|
|
||
|
if (!JsonTokenUtils.IsStartToken(_reader.TokenType))
|
||
|
{
|
||
|
Push(new SchemaScope(JTokenType.None, CurrentMemberSchemas));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch (_reader.TokenType)
|
||
|
{
|
||
|
case JsonToken.StartObject:
|
||
|
ProcessValue();
|
||
|
IList<JsonSchemaModel> objectSchemas = CurrentMemberSchemas.Where(ValidateObject).ToList();
|
||
|
Push(new SchemaScope(JTokenType.Object, objectSchemas));
|
||
|
WriteToken(CurrentSchemas);
|
||
|
break;
|
||
|
case JsonToken.StartArray:
|
||
|
ProcessValue();
|
||
|
IList<JsonSchemaModel> arraySchemas = CurrentMemberSchemas.Where(ValidateArray).ToList();
|
||
|
Push(new SchemaScope(JTokenType.Array, arraySchemas));
|
||
|
WriteToken(CurrentSchemas);
|
||
|
break;
|
||
|
case JsonToken.StartConstructor:
|
||
|
ProcessValue();
|
||
|
Push(new SchemaScope(JTokenType.Constructor, null));
|
||
|
WriteToken(CurrentSchemas);
|
||
|
break;
|
||
|
case JsonToken.PropertyName:
|
||
|
WriteToken(CurrentSchemas);
|
||
|
foreach (JsonSchemaModel schema in CurrentSchemas)
|
||
|
{
|
||
|
ValidatePropertyName(schema);
|
||
|
}
|
||
|
break;
|
||
|
case JsonToken.Raw:
|
||
|
ProcessValue();
|
||
|
break;
|
||
|
case JsonToken.Integer:
|
||
|
ProcessValue();
|
||
|
WriteToken(CurrentMemberSchemas);
|
||
|
foreach (JsonSchemaModel schema in CurrentMemberSchemas)
|
||
|
{
|
||
|
ValidateInteger(schema);
|
||
|
}
|
||
|
break;
|
||
|
case JsonToken.Float:
|
||
|
ProcessValue();
|
||
|
WriteToken(CurrentMemberSchemas);
|
||
|
foreach (JsonSchemaModel schema in CurrentMemberSchemas)
|
||
|
{
|
||
|
ValidateFloat(schema);
|
||
|
}
|
||
|
break;
|
||
|
case JsonToken.String:
|
||
|
ProcessValue();
|
||
|
WriteToken(CurrentMemberSchemas);
|
||
|
foreach (JsonSchemaModel schema in CurrentMemberSchemas)
|
||
|
{
|
||
|
ValidateString(schema);
|
||
|
}
|
||
|
break;
|
||
|
case JsonToken.Boolean:
|
||
|
ProcessValue();
|
||
|
WriteToken(CurrentMemberSchemas);
|
||
|
foreach (JsonSchemaModel schema in CurrentMemberSchemas)
|
||
|
{
|
||
|
ValidateBoolean(schema);
|
||
|
}
|
||
|
break;
|
||
|
case JsonToken.Null:
|
||
|
ProcessValue();
|
||
|
WriteToken(CurrentMemberSchemas);
|
||
|
foreach (JsonSchemaModel schema in CurrentMemberSchemas)
|
||
|
{
|
||
|
ValidateNull(schema);
|
||
|
}
|
||
|
break;
|
||
|
case JsonToken.EndObject:
|
||
|
WriteToken(CurrentSchemas);
|
||
|
foreach (JsonSchemaModel schema in CurrentSchemas)
|
||
|
{
|
||
|
ValidateEndObject(schema);
|
||
|
}
|
||
|
Pop();
|
||
|
break;
|
||
|
case JsonToken.EndArray:
|
||
|
WriteToken(CurrentSchemas);
|
||
|
foreach (JsonSchemaModel schema in CurrentSchemas)
|
||
|
{
|
||
|
ValidateEndArray(schema);
|
||
|
}
|
||
|
Pop();
|
||
|
break;
|
||
|
case JsonToken.EndConstructor:
|
||
|
WriteToken(CurrentSchemas);
|
||
|
Pop();
|
||
|
break;
|
||
|
case JsonToken.Undefined:
|
||
|
case JsonToken.Date:
|
||
|
case JsonToken.Bytes:
|
||
|
// these have no equivalent in JSON schema
|
||
|
WriteToken(CurrentMemberSchemas);
|
||
|
break;
|
||
|
case JsonToken.None:
|
||
|
// no content, do nothing
|
||
|
break;
|
||
|
default:
|
||
|
throw new ArgumentOutOfRangeException();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void WriteToken(IList<JsonSchemaModel> schemas)
|
||
|
{
|
||
|
foreach (SchemaScope schemaScope in _stack)
|
||
|
{
|
||
|
bool isInUniqueArray = (schemaScope.TokenType == JTokenType.Array && schemaScope.IsUniqueArray && schemaScope.ArrayItemCount > 0);
|
||
|
|
||
|
if (isInUniqueArray || schemas.Any(s => s.Enum != null))
|
||
|
{
|
||
|
if (schemaScope.CurrentItemWriter == null)
|
||
|
{
|
||
|
if (JsonTokenUtils.IsEndToken(_reader.TokenType))
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
schemaScope.CurrentItemWriter = new JTokenWriter();
|
||
|
}
|
||
|
|
||
|
schemaScope.CurrentItemWriter.WriteToken(_reader, false);
|
||
|
|
||
|
// finished writing current item
|
||
|
if (schemaScope.CurrentItemWriter.Top == 0 && _reader.TokenType != JsonToken.PropertyName)
|
||
|
{
|
||
|
JToken finishedItem = schemaScope.CurrentItemWriter.Token;
|
||
|
|
||
|
// start next item with new writer
|
||
|
schemaScope.CurrentItemWriter = null;
|
||
|
|
||
|
if (isInUniqueArray)
|
||
|
{
|
||
|
if (schemaScope.UniqueArrayItems.Contains(finishedItem, JToken.EqualityComparer))
|
||
|
{
|
||
|
RaiseError("Non-unique array item at index {0}.".FormatWith(CultureInfo.InvariantCulture, schemaScope.ArrayItemCount - 1), schemaScope.Schemas.First(s => s.UniqueItems));
|
||
|
}
|
||
|
|
||
|
schemaScope.UniqueArrayItems.Add(finishedItem);
|
||
|
}
|
||
|
else if (schemas.Any(s => s.Enum != null))
|
||
|
{
|
||
|
foreach (JsonSchemaModel schema in schemas)
|
||
|
{
|
||
|
if (schema.Enum != null)
|
||
|
{
|
||
|
if (!schema.Enum.ContainsValue(finishedItem, JToken.EqualityComparer))
|
||
|
{
|
||
|
StringWriter sw = new StringWriter(CultureInfo.InvariantCulture);
|
||
|
finishedItem.WriteTo(new JsonTextWriter(sw));
|
||
|
|
||
|
RaiseError("Value {0} is not defined in enum.".FormatWith(CultureInfo.InvariantCulture, sw.ToString()), schema);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void ValidateEndObject(JsonSchemaModel schema)
|
||
|
{
|
||
|
if (schema == null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Dictionary<string, bool> requiredProperties = _currentScope.RequiredProperties;
|
||
|
|
||
|
if (requiredProperties != null && requiredProperties.Values.Any(v => !v))
|
||
|
{
|
||
|
IEnumerable<string> unmatchedRequiredProperties = requiredProperties.Where(kv => !kv.Value).Select(kv => kv.Key);
|
||
|
RaiseError("Required properties are missing from object: {0}.".FormatWith(CultureInfo.InvariantCulture, string.Join(", ", unmatchedRequiredProperties
|
||
|
#if !HAVE_STRING_JOIN_WITH_ENUMERABLE
|
||
|
.ToArray()
|
||
|
#endif
|
||
|
)), schema);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void ValidateEndArray(JsonSchemaModel schema)
|
||
|
{
|
||
|
if (schema == null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
int arrayItemCount = _currentScope.ArrayItemCount;
|
||
|
|
||
|
if (schema.MaximumItems != null && arrayItemCount > schema.MaximumItems)
|
||
|
{
|
||
|
RaiseError("Array item count {0} exceeds maximum count of {1}.".FormatWith(CultureInfo.InvariantCulture, arrayItemCount, schema.MaximumItems), schema);
|
||
|
}
|
||
|
|
||
|
if (schema.MinimumItems != null && arrayItemCount < schema.MinimumItems)
|
||
|
{
|
||
|
RaiseError("Array item count {0} is less than minimum count of {1}.".FormatWith(CultureInfo.InvariantCulture, arrayItemCount, schema.MinimumItems), schema);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void ValidateNull(JsonSchemaModel schema)
|
||
|
{
|
||
|
if (schema == null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!TestType(schema, JsonSchemaType.Null))
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
ValidateNotDisallowed(schema);
|
||
|
}
|
||
|
|
||
|
private void ValidateBoolean(JsonSchemaModel schema)
|
||
|
{
|
||
|
if (schema == null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!TestType(schema, JsonSchemaType.Boolean))
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
ValidateNotDisallowed(schema);
|
||
|
}
|
||
|
|
||
|
private void ValidateString(JsonSchemaModel schema)
|
||
|
{
|
||
|
if (schema == null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!TestType(schema, JsonSchemaType.String))
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
ValidateNotDisallowed(schema);
|
||
|
|
||
|
string value = _reader.Value.ToString();
|
||
|
|
||
|
if (schema.MaximumLength != null && value.Length > schema.MaximumLength)
|
||
|
{
|
||
|
RaiseError("String '{0}' exceeds maximum length of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.MaximumLength), schema);
|
||
|
}
|
||
|
|
||
|
if (schema.MinimumLength != null && value.Length < schema.MinimumLength)
|
||
|
{
|
||
|
RaiseError("String '{0}' is less than minimum length of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.MinimumLength), schema);
|
||
|
}
|
||
|
|
||
|
if (schema.Patterns != null)
|
||
|
{
|
||
|
foreach (string pattern in schema.Patterns)
|
||
|
{
|
||
|
if (!Regex.IsMatch(value, pattern))
|
||
|
{
|
||
|
RaiseError("String '{0}' does not match regex pattern '{1}'.".FormatWith(CultureInfo.InvariantCulture, value, pattern), schema);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void ValidateInteger(JsonSchemaModel schema)
|
||
|
{
|
||
|
if (schema == null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!TestType(schema, JsonSchemaType.Integer))
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
ValidateNotDisallowed(schema);
|
||
|
|
||
|
object value = _reader.Value;
|
||
|
|
||
|
if (schema.Maximum != null)
|
||
|
{
|
||
|
if (JValue.Compare(JTokenType.Integer, value, schema.Maximum) > 0)
|
||
|
{
|
||
|
RaiseError("Integer {0} exceeds maximum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Maximum), schema);
|
||
|
}
|
||
|
if (schema.ExclusiveMaximum && JValue.Compare(JTokenType.Integer, value, schema.Maximum) == 0)
|
||
|
{
|
||
|
RaiseError("Integer {0} equals maximum value of {1} and exclusive maximum is true.".FormatWith(CultureInfo.InvariantCulture, value, schema.Maximum), schema);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (schema.Minimum != null)
|
||
|
{
|
||
|
if (JValue.Compare(JTokenType.Integer, value, schema.Minimum) < 0)
|
||
|
{
|
||
|
RaiseError("Integer {0} is less than minimum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Minimum), schema);
|
||
|
}
|
||
|
if (schema.ExclusiveMinimum && JValue.Compare(JTokenType.Integer, value, schema.Minimum) == 0)
|
||
|
{
|
||
|
RaiseError("Integer {0} equals minimum value of {1} and exclusive minimum is true.".FormatWith(CultureInfo.InvariantCulture, value, schema.Minimum), schema);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (schema.DivisibleBy != null)
|
||
|
{
|
||
|
bool notDivisible;
|
||
|
#if HAVE_BIG_INTEGER
|
||
|
if (value is BigInteger i)
|
||
|
{
|
||
|
// not that this will lose any decimal point on DivisibleBy
|
||
|
// so manually raise an error if DivisibleBy is not an integer and value is not zero
|
||
|
bool divisibleNonInteger = !Math.Abs(schema.DivisibleBy.Value - Math.Truncate(schema.DivisibleBy.Value)).Equals(0);
|
||
|
if (divisibleNonInteger)
|
||
|
{
|
||
|
notDivisible = i != 0;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
notDivisible = i % new BigInteger(schema.DivisibleBy.Value) != 0;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
#endif
|
||
|
{
|
||
|
notDivisible = !IsZero(Convert.ToInt64(value, CultureInfo.InvariantCulture) % schema.DivisibleBy.GetValueOrDefault());
|
||
|
}
|
||
|
|
||
|
if (notDivisible)
|
||
|
{
|
||
|
RaiseError("Integer {0} is not evenly divisible by {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.DivisibleBy), schema);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void ProcessValue()
|
||
|
{
|
||
|
if (_currentScope != null && _currentScope.TokenType == JTokenType.Array)
|
||
|
{
|
||
|
_currentScope.ArrayItemCount++;
|
||
|
|
||
|
foreach (JsonSchemaModel currentSchema in CurrentSchemas)
|
||
|
{
|
||
|
// if there is positional validation and the array index is past the number of item validation schemas and there are no additional items then error
|
||
|
if (currentSchema != null
|
||
|
&& currentSchema.PositionalItemsValidation
|
||
|
&& !currentSchema.AllowAdditionalItems
|
||
|
&& (currentSchema.Items == null || _currentScope.ArrayItemCount - 1 >= currentSchema.Items.Count))
|
||
|
{
|
||
|
RaiseError("Index {0} has not been defined and the schema does not allow additional items.".FormatWith(CultureInfo.InvariantCulture, _currentScope.ArrayItemCount), currentSchema);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void ValidateFloat(JsonSchemaModel schema)
|
||
|
{
|
||
|
if (schema == null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!TestType(schema, JsonSchemaType.Float))
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
ValidateNotDisallowed(schema);
|
||
|
|
||
|
double value = Convert.ToDouble(_reader.Value, CultureInfo.InvariantCulture);
|
||
|
|
||
|
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)
|
||
|
{
|
||
|
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)
|
||
|
{
|
||
|
double remainder = FloatingPointRemainder(value, schema.DivisibleBy.GetValueOrDefault());
|
||
|
|
||
|
if (!IsZero(remainder))
|
||
|
{
|
||
|
RaiseError("Float {0} is not evenly divisible by {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.DivisibleBy), schema);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static double FloatingPointRemainder(double dividend, double divisor)
|
||
|
{
|
||
|
return dividend - Math.Floor(dividend / divisor) * divisor;
|
||
|
}
|
||
|
|
||
|
private static bool IsZero(double value)
|
||
|
{
|
||
|
const double epsilon = 2.2204460492503131e-016;
|
||
|
|
||
|
return Math.Abs(value) < 20.0 * epsilon;
|
||
|
}
|
||
|
|
||
|
private void ValidatePropertyName(JsonSchemaModel schema)
|
||
|
{
|
||
|
if (schema == null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
string propertyName = Convert.ToString(_reader.Value, CultureInfo.InvariantCulture);
|
||
|
|
||
|
if (_currentScope.RequiredProperties.ContainsKey(propertyName))
|
||
|
{
|
||
|
_currentScope.RequiredProperties[propertyName] = true;
|
||
|
}
|
||
|
|
||
|
if (!schema.AllowAdditionalProperties)
|
||
|
{
|
||
|
bool propertyDefinied = IsPropertyDefinied(schema, propertyName);
|
||
|
|
||
|
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)
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return (TestType(schema, JsonSchemaType.Array));
|
||
|
}
|
||
|
|
||
|
private bool ValidateObject(JsonSchemaModel schema)
|
||
|
{
|
||
|
if (schema == null)
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return (TestType(schema, JsonSchemaType.Object));
|
||
|
}
|
||
|
|
||
|
private bool TestType(JsonSchemaModel currentSchema, JsonSchemaType currentType)
|
||
|
{
|
||
|
if (!JsonSchemaGenerator.HasFlag(currentSchema.Type, currentType))
|
||
|
{
|
||
|
RaiseError("Invalid type. Expected {0} but got {1}.".FormatWith(CultureInfo.InvariantCulture, currentSchema.Type, currentType), currentSchema);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
bool IJsonLineInfo.HasLineInfo()
|
||
|
{
|
||
|
return _reader is IJsonLineInfo lineInfo && lineInfo.HasLineInfo();
|
||
|
}
|
||
|
|
||
|
int IJsonLineInfo.LineNumber => (_reader is IJsonLineInfo lineInfo) ? lineInfo.LineNumber : 0;
|
||
|
|
||
|
int IJsonLineInfo.LinePosition => (_reader is IJsonLineInfo lineInfo) ? lineInfo.LinePosition : 0;
|
||
|
}
|
||
|
}
|