JsonSchemaValidator
Class to validate a JSON schema against a given JToken.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NJsonSchema.Validation.FormatValidators;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespace NJsonSchema.Validation
{
public class JsonSchemaValidator
{
private readonly IEnumerable<IFormatValidator> _formatValidators = new IFormatValidator[12] {
new DateTimeFormatValidator(),
new DateFormatValidator(),
new EmailFormatValidator(),
new GuidFormatValidator(),
new HostnameFormatValidator(),
new IpV4FormatValidator(),
new IpV6FormatValidator(),
new TimeFormatValidator(),
new TimeSpanFormatValidator(),
new UriFormatValidator(),
new ByteFormatValidator(),
new Base64FormatValidator()
};
private readonly IDictionary<string, IFormatValidator> _formatValidatorsMap;
private static readonly IEnumerable<JsonObjectType> JsonObjectTypes = (from JsonObjectType t in Enum.GetValues(typeof(JsonObjectType))
where t != JsonObjectType.None
select t).ToList();
public JsonSchemaValidator()
{
_formatValidatorsMap = _formatValidators.ToDictionary((IFormatValidator v) => v.Format, (IFormatValidator v) => v);
}
public ICollection<ValidationError> Validate(string jsonData, JsonSchema schema)
{
using (StringReader reader = new StringReader(jsonData))
using (JsonTextReader reader2 = new JsonTextReader(reader) {
DateParseHandling = DateParseHandling.None
}) {
JToken token = JToken.ReadFrom(reader2);
return Validate(token, schema);
}
}
public ICollection<ValidationError> Validate(JToken token, JsonSchema schema)
{
return Validate(token, schema.ActualSchema, null, token.Path);
}
protected virtual ICollection<ValidationError> Validate(JToken token, JsonSchema schema, string propertyName, string propertyPath)
{
List<ValidationError> list = new List<ValidationError>();
ValidateAnyOf(token, schema, propertyName, propertyPath, list);
ValidateAllOf(token, schema, propertyName, propertyPath, list);
ValidateOneOf(token, schema, propertyName, propertyPath, list);
ValidateNot(token, schema, propertyName, propertyPath, list);
ValidateType(token, schema, propertyName, propertyPath, list);
ValidateEnum(token, schema, propertyName, propertyPath, list);
ValidateProperties(token, schema, propertyName, propertyPath, list);
return list;
}
private void ValidateType(JToken token, JsonSchema schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
Dictionary<JsonObjectType, ICollection<ValidationError>> dictionary = GetTypes(schema).ToDictionary((Func<JsonObjectType, JsonObjectType>)((JsonObjectType t) => t), (Func<JsonObjectType, ICollection<ValidationError>>)((JsonObjectType t) => new List<ValidationError>()));
if (dictionary.Count > 1) {
foreach (KeyValuePair<JsonObjectType, ICollection<ValidationError>> item in dictionary) {
ValidateArray(token, schema, item.Key, propertyName, propertyPath, (List<ValidationError>)item.Value);
ValidateString(token, schema, item.Key, propertyName, propertyPath, (List<ValidationError>)item.Value);
ValidateNumber(token, schema, item.Key, propertyName, propertyPath, (List<ValidationError>)item.Value);
ValidateInteger(token, schema, item.Key, propertyName, propertyPath, (List<ValidationError>)item.Value);
ValidateBoolean(token, schema, item.Key, propertyName, propertyPath, (List<ValidationError>)item.Value);
ValidateNull(token, schema, item.Key, propertyName, propertyPath, (List<ValidationError>)item.Value);
ValidateObject(token, schema, item.Key, propertyName, propertyPath, (List<ValidationError>)item.Value);
}
if (dictionary.All((KeyValuePair<JsonObjectType, ICollection<ValidationError>> t) => t.Value.Count > 0))
errors.Add(new MultiTypeValidationError(ValidationErrorKind.NoTypeValidates, propertyName, propertyPath, dictionary, token, schema));
} else {
ValidateArray(token, schema, schema.Type, propertyName, propertyPath, errors);
ValidateString(token, schema, schema.Type, propertyName, propertyPath, errors);
ValidateNumber(token, schema, schema.Type, propertyName, propertyPath, errors);
ValidateInteger(token, schema, schema.Type, propertyName, propertyPath, errors);
ValidateBoolean(token, schema, schema.Type, propertyName, propertyPath, errors);
ValidateNull(token, schema, schema.Type, propertyName, propertyPath, errors);
ValidateObject(token, schema, schema.Type, propertyName, propertyPath, errors);
}
}
private IEnumerable<JsonObjectType> GetTypes(JsonSchema schema)
{
return from t in JsonObjectTypes
where schema.Type.HasFlag(t)
select t;
}
private void ValidateAnyOf(JToken token, JsonSchema schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (schema.AnyOf.Count > 0) {
Dictionary<JsonSchema, ICollection<ValidationError>> dictionary = schema.AnyOf.ToDictionary((JsonSchema s) => s, (JsonSchema s) => Validate(token, s));
if (dictionary.All((KeyValuePair<JsonSchema, ICollection<ValidationError>> s) => s.Value.Count != 0))
errors.Add(new ChildSchemaValidationError(ValidationErrorKind.NotAnyOf, propertyName, propertyPath, dictionary, token, schema));
}
}
private void ValidateAllOf(JToken token, JsonSchema schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (schema.AllOf.Count > 0) {
Dictionary<JsonSchema, ICollection<ValidationError>> dictionary = schema.AllOf.ToDictionary((JsonSchema s) => s, (JsonSchema s) => Validate(token, s));
if (dictionary.Any((KeyValuePair<JsonSchema, ICollection<ValidationError>> s) => s.Value.Count != 0))
errors.Add(new ChildSchemaValidationError(ValidationErrorKind.NotAllOf, propertyName, propertyPath, dictionary, token, schema));
}
}
private void ValidateOneOf(JToken token, JsonSchema schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (schema.OneOf.Count > 0) {
Dictionary<JsonSchema, ICollection<ValidationError>> dictionary = schema.OneOf.ToDictionary((JsonSchema s) => s, (JsonSchema s) => Validate(token, s));
if (dictionary.Count((KeyValuePair<JsonSchema, ICollection<ValidationError>> s) => s.Value.Count == 0) != 1)
errors.Add(new ChildSchemaValidationError(ValidationErrorKind.NotOneOf, propertyName, propertyPath, dictionary, token, schema));
}
}
private void ValidateNot(JToken token, JsonSchema schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (schema.Not != null && Validate(token, schema.Not).Count == 0)
errors.Add(new ValidationError(ValidationErrorKind.ExcludedSchemaValidates, propertyName, propertyPath, token, schema));
}
private void ValidateNull(JToken token, JsonSchema schema, JsonObjectType type, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (type.HasFlag(JsonObjectType.Null) && token != null && token.Type != JTokenType.Null)
errors.Add(new ValidationError(ValidationErrorKind.NullExpected, propertyName, propertyPath, token, schema));
}
private void ValidateEnum(JToken token, JsonSchema schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (schema.Enumeration.Contains(null)) {
JToken jToken = token;
if (jToken != null && jToken.Type == JTokenType.Null)
return;
}
if (schema.Enumeration.Count > 0 && schema.Enumeration.All((object v) => v?.ToString() != token?.ToString()))
errors.Add(new ValidationError(ValidationErrorKind.NotInEnumeration, propertyName, propertyPath, token, schema));
}
private void ValidateString(JToken token, JsonSchema schema, JsonObjectType type, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (token.Type == JTokenType.String || token.Type == JTokenType.Date || token.Type == JTokenType.Guid || token.Type == JTokenType.TimeSpan || token.Type == JTokenType.Uri) {
string text = (token.Type == JTokenType.Date) ? (token as JValue).ToString("yyyy-MM-ddTHH:mm:ssK") : token.Value<string>();
if (text != null) {
if (!string.IsNullOrEmpty(schema.Pattern) && !Regex.IsMatch(text, schema.Pattern))
errors.Add(new ValidationError(ValidationErrorKind.PatternMismatch, propertyName, propertyPath, token, schema));
if (schema.MinLength.HasValue) {
int length = text.Length;
int? minLength = schema.MinLength;
if ((length < minLength.GetValueOrDefault()) & minLength.HasValue)
errors.Add(new ValidationError(ValidationErrorKind.StringTooShort, propertyName, propertyPath, token, schema));
}
if (schema.MaxLength.HasValue) {
int length2 = text.Length;
int? minLength = schema.MaxLength;
if ((length2 > minLength.GetValueOrDefault()) & minLength.HasValue)
errors.Add(new ValidationError(ValidationErrorKind.StringTooLong, propertyName, propertyPath, token, schema));
}
IFormatValidator value;
if (!string.IsNullOrEmpty(schema.Format) && _formatValidatorsMap.TryGetValue(schema.Format, out value) && !value.IsValid(text, token.Type)) {
ValidationError item = new ValidationError(value.ValidationErrorKind, propertyName, propertyPath, token, schema);
errors.Add(item);
}
}
} else if (type.HasFlag(JsonObjectType.String)) {
errors.Add(new ValidationError(ValidationErrorKind.StringExpected, propertyName, propertyPath, token, schema));
}
}
private void ValidateNumber(JToken token, JsonSchema schema, JsonObjectType type, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (type.HasFlag(JsonObjectType.Number) && token.Type != JTokenType.Float && token.Type != JTokenType.Integer)
errors.Add(new ValidationError(ValidationErrorKind.NumberExpected, propertyName, propertyPath, token, schema));
if (token.Type == JTokenType.Float || token.Type == JTokenType.Integer)
try {
decimal num = token.Value<decimal>();
if (schema.Minimum.HasValue) {
int num2;
if (!schema.IsExclusiveMinimum) {
decimal d = num;
decimal? minimum = schema.Minimum;
num2 = (((d < minimum.GetValueOrDefault()) & minimum.HasValue) ? 1 : 0);
} else {
decimal d2 = num;
decimal? minimum = schema.Minimum;
num2 = (((d2 <= minimum.GetValueOrDefault()) & minimum.HasValue) ? 1 : 0);
}
if (num2 != 0)
errors.Add(new ValidationError(ValidationErrorKind.NumberTooSmall, propertyName, propertyPath, token, schema));
}
if (schema.Maximum.HasValue) {
int num3;
if (!schema.IsExclusiveMaximum) {
decimal d3 = num;
decimal? minimum = schema.Maximum;
num3 = (((d3 > minimum.GetValueOrDefault()) & minimum.HasValue) ? 1 : 0);
} else {
decimal d4 = num;
decimal? minimum = schema.Maximum;
num3 = (((d4 >= minimum.GetValueOrDefault()) & minimum.HasValue) ? 1 : 0);
}
if (num3 != 0)
errors.Add(new ValidationError(ValidationErrorKind.NumberTooBig, propertyName, propertyPath, token, schema));
}
if (schema.ExclusiveMinimum.HasValue) {
decimal d5 = num;
decimal? minimum = schema.ExclusiveMinimum;
if ((d5 <= minimum.GetValueOrDefault()) & minimum.HasValue)
errors.Add(new ValidationError(ValidationErrorKind.NumberTooSmall, propertyName, propertyPath, token, schema));
}
if (schema.ExclusiveMaximum.HasValue) {
decimal d6 = num;
decimal? minimum = schema.ExclusiveMaximum;
if ((d6 >= minimum.GetValueOrDefault()) & minimum.HasValue)
errors.Add(new ValidationError(ValidationErrorKind.NumberTooBig, propertyName, propertyPath, token, schema));
}
if (schema.MultipleOf.HasValue) {
decimal value = num;
decimal? multipleOf = schema.MultipleOf;
decimal? minimum = (decimal?)value % multipleOf;
if (!((minimum.GetValueOrDefault() == default(decimal)) & minimum.HasValue))
errors.Add(new ValidationError(ValidationErrorKind.NumberNotMultipleOf, propertyName, propertyPath, token, schema));
}
} catch (OverflowException) {
double num4 = token.Value<double>();
if (schema.Minimum.HasValue && (schema.IsExclusiveMinimum ? (num4 <= (double)schema.Minimum.Value) : (num4 < (double)schema.Minimum.Value)))
errors.Add(new ValidationError(ValidationErrorKind.NumberTooSmall, propertyName, propertyPath, token, schema));
if (schema.Maximum.HasValue && (schema.IsExclusiveMaximum ? (num4 >= (double)schema.Maximum.Value) : (num4 > (double)schema.Maximum.Value)))
errors.Add(new ValidationError(ValidationErrorKind.NumberTooBig, propertyName, propertyPath, token, schema));
if (schema.ExclusiveMinimum.HasValue && num4 <= (double)schema.ExclusiveMinimum.Value)
errors.Add(new ValidationError(ValidationErrorKind.NumberTooSmall, propertyName, propertyPath, token, schema));
if (schema.ExclusiveMaximum.HasValue && num4 >= (double)schema.ExclusiveMaximum.Value)
errors.Add(new ValidationError(ValidationErrorKind.NumberTooBig, propertyName, propertyPath, token, schema));
if (schema.MultipleOf.HasValue && num4 % (double)schema.MultipleOf.Value != 0)
errors.Add(new ValidationError(ValidationErrorKind.NumberNotMultipleOf, propertyName, propertyPath, token, schema));
}
}
private void ValidateInteger(JToken token, JsonSchema schema, JsonObjectType type, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (type.HasFlag(JsonObjectType.Integer) && token.Type != JTokenType.Integer)
errors.Add(new ValidationError(ValidationErrorKind.IntegerExpected, propertyName, propertyPath, token, schema));
}
private void ValidateBoolean(JToken token, JsonSchema schema, JsonObjectType type, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (type.HasFlag(JsonObjectType.Boolean) && token.Type != JTokenType.Boolean)
errors.Add(new ValidationError(ValidationErrorKind.BooleanExpected, propertyName, propertyPath, token, schema));
}
private void ValidateObject(JToken token, JsonSchema schema, JsonObjectType type, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (type.HasFlag(JsonObjectType.Object) && !(token is JObject))
errors.Add(new ValidationError(ValidationErrorKind.ObjectExpected, propertyName, propertyPath, token, schema));
}
private void ValidateProperties(JToken token, JsonSchema schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
JObject jObject = token as JObject;
if (jObject != null || !schema.Type.HasFlag(JsonObjectType.Null)) {
foreach (KeyValuePair<string, JsonSchemaProperty> property in schema.Properties) {
string propertyPath2 = GetPropertyPath(propertyPath, property.Key);
JProperty jProperty = jObject?.Property(property.Key);
if (jProperty != null) {
ICollection<ValidationError> collection = Validate(jProperty.Value, property.Value.ActualSchema, property.Key, propertyPath2);
errors.AddRange(collection);
} else if (property.Value.IsRequired) {
errors.Add(new ValidationError(ValidationErrorKind.PropertyRequired, property.Key, propertyPath2, token, schema));
}
}
foreach (string requiredProperty in schema.RequiredProperties) {
if (!schema.Properties.ContainsKey(requiredProperty)) {
string propertyPath3 = GetPropertyPath(propertyPath, requiredProperty);
if (jObject?.Property(requiredProperty) == null)
errors.Add(new ValidationError(ValidationErrorKind.PropertyRequired, requiredProperty, propertyPath3, token, schema));
}
}
if (jObject != null) {
List<JProperty> list = jObject.Properties().ToList();
ValidateMaxProperties(token, list, schema, propertyName, propertyPath, errors);
ValidateMinProperties(token, list, schema, propertyName, propertyPath, errors);
List<JProperty> additionalProperties = (from p in list
where !schema.Properties.ContainsKey(p.Name)
select p).ToList();
ValidatePatternProperties(additionalProperties, schema, errors);
ValidateAdditionalProperties(token, additionalProperties, schema, propertyName, propertyPath, errors);
}
}
}
private string GetPropertyPath(string propertyPath, string propertyName)
{
if (string.IsNullOrEmpty(propertyPath))
return propertyName;
return propertyPath + "." + propertyName;
}
private void ValidateMaxProperties(JToken token, IList<JProperty> properties, JsonSchema schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (schema.MaxProperties > 0 && properties.Count() > schema.MaxProperties)
errors.Add(new ValidationError(ValidationErrorKind.TooManyProperties, propertyName, propertyPath, token, schema));
}
private void ValidateMinProperties(JToken token, IList<JProperty> properties, JsonSchema schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (schema.MinProperties > 0 && properties.Count() < schema.MinProperties)
errors.Add(new ValidationError(ValidationErrorKind.TooFewProperties, propertyName, propertyPath, token, schema));
}
private void ValidatePatternProperties(List<JProperty> additionalProperties, JsonSchema schema, List<ValidationError> errors)
{
JProperty[] array = additionalProperties.ToArray();
foreach (JProperty property in array) {
KeyValuePair<string, JsonSchemaProperty> keyValuePair = schema.PatternProperties.FirstOrDefault((KeyValuePair<string, JsonSchemaProperty> p) => Regex.IsMatch(property.Name, p.Key));
if (keyValuePair.Value != null) {
ChildSchemaValidationError childSchemaValidationError = TryCreateChildSchemaError(property.Value, keyValuePair.Value, ValidationErrorKind.AdditionalPropertiesNotValid, property.Name, property.Path);
if (childSchemaValidationError != null)
errors.Add(childSchemaValidationError);
additionalProperties.Remove(property);
}
}
}
private void ValidateAdditionalProperties(JToken token, List<JProperty> additionalProperties, JsonSchema schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
List<JProperty>.Enumerator enumerator;
if (schema.AdditionalPropertiesSchema != null) {
enumerator = additionalProperties.GetEnumerator();
try {
while (enumerator.MoveNext()) {
JProperty current = enumerator.Current;
ChildSchemaValidationError childSchemaValidationError = TryCreateChildSchemaError(current.Value, schema.AdditionalPropertiesSchema, ValidationErrorKind.AdditionalPropertiesNotValid, current.Name, current.Path);
if (childSchemaValidationError != null)
errors.Add(childSchemaValidationError);
}
} finally {
((IDisposable)enumerator).Dispose();
}
} else if (!schema.AllowAdditionalProperties && additionalProperties.Any()) {
enumerator = additionalProperties.GetEnumerator();
try {
while (enumerator.MoveNext()) {
JProperty current2 = enumerator.Current;
string propertyPath2 = (!string.IsNullOrEmpty(propertyPath)) ? (propertyPath + "." + current2.Name) : current2.Name;
errors.Add(new ValidationError(ValidationErrorKind.NoAdditionalPropertiesAllowed, current2.Name, propertyPath2, current2, schema));
}
} finally {
((IDisposable)enumerator).Dispose();
}
}
}
private void ValidateArray(JToken token, JsonSchema schema, JsonObjectType type, string propertyName, string propertyPath, List<ValidationError> errors)
{
JArray jArray = token as JArray;
if (jArray != null) {
if (schema.MinItems > 0 && jArray.Count < schema.MinItems)
errors.Add(new ValidationError(ValidationErrorKind.TooFewItems, propertyName, propertyPath, token, schema));
if (schema.MaxItems > 0 && jArray.Count > schema.MaxItems)
errors.Add(new ValidationError(ValidationErrorKind.TooManyItems, propertyName, propertyPath, token, schema));
if (schema.UniqueItems && jArray.Count != (from a in jArray
select a.ToString()).Distinct().Count())
errors.Add(new ValidationError(ValidationErrorKind.ItemsNotUnique, propertyName, propertyPath, token, schema));
for (int i = 0; i < jArray.Count; i++) {
JToken jToken = jArray[i];
string text = $"""{i}""";
string path = (!string.IsNullOrEmpty(propertyPath)) ? (propertyPath + text) : text;
if (schema.Item != null) {
ChildSchemaValidationError childSchemaValidationError = TryCreateChildSchemaError(jToken, schema.Item, ValidationErrorKind.ArrayItemNotValid, text, path);
if (childSchemaValidationError != null)
errors.Add(childSchemaValidationError);
}
ValidateAdditionalItems(jToken, schema, i, propertyPath, errors);
}
} else if (type.HasFlag(JsonObjectType.Array)) {
errors.Add(new ValidationError(ValidationErrorKind.ArrayExpected, propertyName, propertyPath, token, schema));
}
}
private void ValidateAdditionalItems(JToken item, JsonSchema schema, int index, string propertyPath, List<ValidationError> errors)
{
if (schema.Items.Count > 0) {
string text = $"""{index}""";
if (schema.Items.Count > index) {
ChildSchemaValidationError childSchemaValidationError = TryCreateChildSchemaError(item, schema.Items.ElementAt(index), ValidationErrorKind.ArrayItemNotValid, text, propertyPath + text);
if (childSchemaValidationError != null)
errors.Add(childSchemaValidationError);
} else if (schema.AdditionalItemsSchema != null) {
ChildSchemaValidationError childSchemaValidationError2 = TryCreateChildSchemaError(item, schema.AdditionalItemsSchema, ValidationErrorKind.AdditionalItemNotValid, text, propertyPath + text);
if (childSchemaValidationError2 != null)
errors.Add(childSchemaValidationError2);
} else if (!schema.AllowAdditionalItems) {
errors.Add(new ValidationError(ValidationErrorKind.TooManyItemsInTuple, text, propertyPath + text, item, schema));
}
}
}
private ChildSchemaValidationError TryCreateChildSchemaError(JToken token, JsonSchema schema, ValidationErrorKind errorKind, string property, string path)
{
ICollection<ValidationError> collection = Validate(token, schema.ActualSchema, null, path);
if (collection.Count == 0)
return null;
Dictionary<JsonSchema, ICollection<ValidationError>> dictionary = new Dictionary<JsonSchema, ICollection<ValidationError>>();
dictionary.Add(schema, collection);
return new ChildSchemaValidationError(errorKind, property, path, dictionary, token, schema);
}
}
}