JsonSchemaValidator
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace NJsonSchema.Validation
{
public class JsonSchemaValidator
{
private static readonly IEnumerable<JsonObjectType> JsonObjectTypes = (from JsonObjectType t in Enum.GetValues(typeof(JsonObjectType))
where t != JsonObjectType.None
select t).ToList();
public ICollection<ValidationError> Validate(string jsonData, JsonSchema4 schema)
{
JToken token = JToken.Parse(jsonData);
return Validate(token, schema);
}
public ICollection<ValidationError> Validate(JToken token, JsonSchema4 schema)
{
return Validate(token, schema.ActualSchema, null, token.Path);
}
protected virtual ICollection<ValidationError> Validate(JToken token, JsonSchema4 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, JsonSchema4 schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
Dictionary<JsonObjectType, List<ValidationError>> dictionary = GetTypes(schema).ToDictionary((JsonObjectType t) => t, (JsonObjectType t) => new List<ValidationError>());
if (dictionary.Count > 0) {
foreach (KeyValuePair<JsonObjectType, List<ValidationError>> item in dictionary) {
ValidateArray(token, schema, item.Key, propertyName, propertyPath, item.Value);
ValidateString(token, schema, item.Key, propertyName, propertyPath, item.Value);
ValidateNumber(token, schema, item.Key, propertyName, propertyPath, item.Value);
ValidateInteger(token, schema, item.Key, propertyName, propertyPath, item.Value);
ValidateBoolean(token, schema, item.Key, propertyName, propertyPath, item.Value);
ValidateNull(token, schema, item.Key, propertyName, propertyPath, item.Value);
ValidateObject(token, schema, item.Key, propertyName, propertyPath, item.Value);
}
if (dictionary.All((KeyValuePair<JsonObjectType, List<ValidationError>> t) => t.Value.Count > 0))
errors.AddRange(dictionary.SelectMany((KeyValuePair<JsonObjectType, List<ValidationError>> t) => t.Value));
} 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(JsonSchema4 schema)
{
return from t in JsonObjectTypes
where schema.Type.HasFlag(t)
select t;
}
private void ValidateAnyOf(JToken token, JsonSchema4 schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (schema.AnyOf.Count > 0) {
Dictionary<JsonSchema4, ICollection<ValidationError>> dictionary = schema.AnyOf.ToDictionary((JsonSchema4 s) => s, (JsonSchema4 s) => Validate(token, s));
if (dictionary.All((KeyValuePair<JsonSchema4, ICollection<ValidationError>> s) => s.Value.Count != 0))
errors.Add(new ChildSchemaValidationError(ValidationErrorKind.NotAnyOf, propertyName, propertyPath, dictionary, token, schema));
}
}
private void ValidateAllOf(JToken token, JsonSchema4 schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (schema.AllOf.Count > 0) {
Dictionary<JsonSchema4, ICollection<ValidationError>> dictionary = schema.AllOf.ToDictionary((JsonSchema4 s) => s, (JsonSchema4 s) => Validate(token, s));
if (dictionary.Any((KeyValuePair<JsonSchema4, ICollection<ValidationError>> s) => s.Value.Count != 0))
errors.Add(new ChildSchemaValidationError(ValidationErrorKind.NotAllOf, propertyName, propertyPath, dictionary, token, schema));
}
}
private void ValidateOneOf(JToken token, JsonSchema4 schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
if (schema.OneOf.Count > 0) {
Dictionary<JsonSchema4, ICollection<ValidationError>> dictionary = schema.OneOf.ToDictionary((JsonSchema4 s) => s, (JsonSchema4 s) => Validate(token, s));
if (dictionary.Count((KeyValuePair<JsonSchema4, ICollection<ValidationError>> s) => s.Value.Count == 0) != 1)
errors.Add(new ChildSchemaValidationError(ValidationErrorKind.NotOneOf, propertyName, propertyPath, dictionary, token, schema));
}
}
private void ValidateNot(JToken token, JsonSchema4 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, JsonSchema4 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, JsonSchema4 schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
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, JsonSchema4 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 && text.Length < schema.MinLength)
errors.Add(new ValidationError(ValidationErrorKind.StringTooShort, propertyName, propertyPath, token, schema));
if (schema.MaxLength.HasValue && text.Length > schema.MaxLength)
errors.Add(new ValidationError(ValidationErrorKind.StringTooLong, propertyName, propertyPath, token, schema));
if (!string.IsNullOrEmpty(schema.Format)) {
if (schema.Format == "date-time" && token.Type != JTokenType.Date && !DateTime.TryParse(text, out DateTime _))
errors.Add(new ValidationError(ValidationErrorKind.DateTimeExpected, propertyName, propertyPath, token, schema));
if (schema.Format == "uri" && token.Type != JTokenType.Uri && !Uri.TryCreate(text, UriKind.Absolute, out Uri _))
errors.Add(new ValidationError(ValidationErrorKind.UriExpected, propertyName, propertyPath, token, schema));
if (schema.Format == "email" && !Regex.IsMatch(text, "^\\A(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\\Z$", RegexOptions.IgnoreCase))
errors.Add(new ValidationError(ValidationErrorKind.EmailExpected, propertyName, propertyPath, token, schema));
if (schema.Format == "ipv4" && !Regex.IsMatch(text, "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", RegexOptions.IgnoreCase))
errors.Add(new ValidationError(ValidationErrorKind.IpV4Expected, propertyName, propertyPath, token, schema));
if (schema.Format == "ipv6" && Uri.CheckHostName(text) != UriHostNameType.IPv6)
errors.Add(new ValidationError(ValidationErrorKind.IpV6Expected, propertyName, propertyPath, token, schema));
if (schema.Format == "guid" && !Guid.TryParse(text, out Guid _))
errors.Add(new ValidationError(ValidationErrorKind.GuidExpected, propertyName, propertyPath, token, schema));
if (schema.Format == "hostname" && !Regex.IsMatch(text, "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$", RegexOptions.IgnoreCase))
errors.Add(new ValidationError(ValidationErrorKind.HostnameExpected, propertyName, propertyPath, token, schema));
if ((schema.Format == "byte" || schema.Format == "base64") && (text.Length % 4 != 0 || !Regex.IsMatch(text, "^[a-zA-Z0-9\\+/]*={0,3}$", RegexOptions.None)))
errors.Add(new ValidationError(ValidationErrorKind.Base64Expected, propertyName, propertyPath, token, schema));
}
}
} else if (type.HasFlag(JsonObjectType.String)) {
errors.Add(new ValidationError(ValidationErrorKind.StringExpected, propertyName, propertyPath, token, schema));
}
}
private void ValidateNumber(JToken token, JsonSchema4 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) {
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.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));
}
}
}
private void ValidateInteger(JToken token, JsonSchema4 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, JsonSchema4 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, JsonSchema4 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, JsonSchema4 schema, string propertyName, string propertyPath, List<ValidationError> errors)
{
JObject jObject = token as JObject;
if (jObject != null || !schema.Type.HasFlag(JsonObjectType.Null)) {
foreach (KeyValuePair<string, JsonProperty> property in schema.Properties) {
string propertyPath2 = (!string.IsNullOrEmpty(propertyPath)) ? (propertyPath + "." + property.Key) : 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));
}
}
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 void ValidateMaxProperties(JToken token, IList<JProperty> properties, JsonSchema4 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, JsonSchema4 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, JsonSchema4 schema, List<ValidationError> errors)
{
JProperty[] array = additionalProperties.ToArray();
foreach (JProperty property in array) {
KeyValuePair<string, JsonSchema4> keyValuePair = schema.PatternProperties.FirstOrDefault((KeyValuePair<string, JsonSchema4> 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, JsonSchema4 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, JsonSchema4 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, JsonSchema4 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, JsonSchema4 schema, ValidationErrorKind errorKind, string property, string path)
{
ICollection<ValidationError> collection = Validate(token, schema.ActualSchema, null, path);
if (collection.Count == 0)
return null;
Dictionary<JsonSchema4, ICollection<ValidationError>> dictionary = new Dictionary<JsonSchema4, ICollection<ValidationError>>();
dictionary.Add(schema, collection);
return new ChildSchemaValidationError(errorKind, property, path, dictionary, token, schema);
}
}
}