487 lines
19 KiB
C#
487 lines
19 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;
|
|
using LC.Newtonsoft.Json.Serialization;
|
|
#if !HAVE_LINQ
|
|
using LC.Newtonsoft.Json.Utilities.LinqBridge;
|
|
#else
|
|
using System.Linq;
|
|
#endif
|
|
using System.Globalization;
|
|
using LC.Newtonsoft.Json.Utilities;
|
|
using LC.Newtonsoft.Json.Linq;
|
|
|
|
namespace LC.Newtonsoft.Json.Schema
|
|
{
|
|
[Obsolete("JSON Schema validation has been moved to its own package. See https://www.newtonsoft.com/jsonschema for more details.")]
|
|
internal class JsonSchemaBuilder
|
|
{
|
|
private readonly IList<JsonSchema> _stack;
|
|
private readonly JsonSchemaResolver _resolver;
|
|
private readonly IDictionary<string, JsonSchema> _documentSchemas;
|
|
private JsonSchema _currentSchema;
|
|
private JObject _rootSchema;
|
|
|
|
public JsonSchemaBuilder(JsonSchemaResolver resolver)
|
|
{
|
|
_stack = new List<JsonSchema>();
|
|
_documentSchemas = new Dictionary<string, JsonSchema>();
|
|
_resolver = resolver;
|
|
}
|
|
|
|
private void Push(JsonSchema value)
|
|
{
|
|
_currentSchema = value;
|
|
_stack.Add(value);
|
|
_resolver.LoadedSchemas.Add(value);
|
|
_documentSchemas.Add(value.Location, value);
|
|
}
|
|
|
|
private JsonSchema Pop()
|
|
{
|
|
JsonSchema poppedSchema = _currentSchema;
|
|
_stack.RemoveAt(_stack.Count - 1);
|
|
_currentSchema = _stack.LastOrDefault();
|
|
|
|
return poppedSchema;
|
|
}
|
|
|
|
private JsonSchema CurrentSchema => _currentSchema;
|
|
|
|
internal JsonSchema Read(JsonReader reader)
|
|
{
|
|
JToken schemaToken = JToken.ReadFrom(reader);
|
|
|
|
_rootSchema = schemaToken as JObject;
|
|
|
|
JsonSchema schema = BuildSchema(schemaToken);
|
|
|
|
ResolveReferences(schema);
|
|
|
|
return schema;
|
|
}
|
|
|
|
private string UnescapeReference(string reference)
|
|
{
|
|
return Uri.UnescapeDataString(reference).Replace("~1", "/").Replace("~0", "~");
|
|
}
|
|
|
|
private JsonSchema ResolveReferences(JsonSchema schema)
|
|
{
|
|
if (schema.DeferredReference != null)
|
|
{
|
|
string reference = schema.DeferredReference;
|
|
|
|
bool locationReference = (reference.StartsWith("#", StringComparison.Ordinal));
|
|
if (locationReference)
|
|
{
|
|
reference = UnescapeReference(reference);
|
|
}
|
|
|
|
JsonSchema resolvedSchema = _resolver.GetSchema(reference);
|
|
|
|
if (resolvedSchema == null)
|
|
{
|
|
if (locationReference)
|
|
{
|
|
string[] escapedParts = schema.DeferredReference.TrimStart('#').Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
|
JToken currentToken = _rootSchema;
|
|
foreach (string escapedPart in escapedParts)
|
|
{
|
|
string part = UnescapeReference(escapedPart);
|
|
|
|
if (currentToken.Type == JTokenType.Object)
|
|
{
|
|
currentToken = currentToken[part];
|
|
}
|
|
else if (currentToken.Type == JTokenType.Array || currentToken.Type == JTokenType.Constructor)
|
|
{
|
|
if (int.TryParse(part, out int index) && index >= 0 && index < currentToken.Count())
|
|
{
|
|
currentToken = currentToken[index];
|
|
}
|
|
else
|
|
{
|
|
currentToken = null;
|
|
}
|
|
}
|
|
|
|
if (currentToken == null)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (currentToken != null)
|
|
{
|
|
resolvedSchema = BuildSchema(currentToken);
|
|
}
|
|
}
|
|
|
|
if (resolvedSchema == null)
|
|
{
|
|
throw new JsonException("Could not resolve schema reference '{0}'.".FormatWith(CultureInfo.InvariantCulture, schema.DeferredReference));
|
|
}
|
|
}
|
|
|
|
schema = resolvedSchema;
|
|
}
|
|
|
|
if (schema.ReferencesResolved)
|
|
{
|
|
return schema;
|
|
}
|
|
|
|
schema.ReferencesResolved = true;
|
|
|
|
if (schema.Extends != null)
|
|
{
|
|
for (int i = 0; i < schema.Extends.Count; i++)
|
|
{
|
|
schema.Extends[i] = ResolveReferences(schema.Extends[i]);
|
|
}
|
|
}
|
|
|
|
if (schema.Items != null)
|
|
{
|
|
for (int i = 0; i < schema.Items.Count; i++)
|
|
{
|
|
schema.Items[i] = ResolveReferences(schema.Items[i]);
|
|
}
|
|
}
|
|
|
|
if (schema.AdditionalItems != null)
|
|
{
|
|
schema.AdditionalItems = ResolveReferences(schema.AdditionalItems);
|
|
}
|
|
|
|
if (schema.PatternProperties != null)
|
|
{
|
|
foreach (KeyValuePair<string, JsonSchema> patternProperty in schema.PatternProperties.ToList())
|
|
{
|
|
schema.PatternProperties[patternProperty.Key] = ResolveReferences(patternProperty.Value);
|
|
}
|
|
}
|
|
|
|
if (schema.Properties != null)
|
|
{
|
|
foreach (KeyValuePair<string, JsonSchema> property in schema.Properties.ToList())
|
|
{
|
|
schema.Properties[property.Key] = ResolveReferences(property.Value);
|
|
}
|
|
}
|
|
|
|
if (schema.AdditionalProperties != null)
|
|
{
|
|
schema.AdditionalProperties = ResolveReferences(schema.AdditionalProperties);
|
|
}
|
|
|
|
return schema;
|
|
}
|
|
|
|
private JsonSchema BuildSchema(JToken token)
|
|
{
|
|
if (!(token is JObject schemaObject))
|
|
{
|
|
throw JsonException.Create(token, token.Path, "Expected object while parsing schema object, got {0}.".FormatWith(CultureInfo.InvariantCulture, token.Type));
|
|
}
|
|
|
|
if (schemaObject.TryGetValue(JsonTypeReflector.RefPropertyName, out JToken referenceToken))
|
|
{
|
|
JsonSchema deferredSchema = new JsonSchema();
|
|
deferredSchema.DeferredReference = (string)referenceToken;
|
|
|
|
return deferredSchema;
|
|
}
|
|
|
|
string location = token.Path.Replace(".", "/").Replace("[", "/").Replace("]", string.Empty);
|
|
if (!string.IsNullOrEmpty(location))
|
|
{
|
|
location = "/" + location;
|
|
}
|
|
location = "#" + location;
|
|
|
|
if (_documentSchemas.TryGetValue(location, out JsonSchema existingSchema))
|
|
{
|
|
return existingSchema;
|
|
}
|
|
|
|
Push(new JsonSchema { Location = location });
|
|
|
|
ProcessSchemaProperties(schemaObject);
|
|
|
|
return Pop();
|
|
}
|
|
|
|
private void ProcessSchemaProperties(JObject schemaObject)
|
|
{
|
|
foreach (KeyValuePair<string, JToken> property in schemaObject)
|
|
{
|
|
switch (property.Key)
|
|
{
|
|
case JsonSchemaConstants.TypePropertyName:
|
|
CurrentSchema.Type = ProcessType(property.Value);
|
|
break;
|
|
case JsonSchemaConstants.IdPropertyName:
|
|
CurrentSchema.Id = (string)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.TitlePropertyName:
|
|
CurrentSchema.Title = (string)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.DescriptionPropertyName:
|
|
CurrentSchema.Description = (string)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.PropertiesPropertyName:
|
|
CurrentSchema.Properties = ProcessProperties(property.Value);
|
|
break;
|
|
case JsonSchemaConstants.ItemsPropertyName:
|
|
ProcessItems(property.Value);
|
|
break;
|
|
case JsonSchemaConstants.AdditionalPropertiesPropertyName:
|
|
ProcessAdditionalProperties(property.Value);
|
|
break;
|
|
case JsonSchemaConstants.AdditionalItemsPropertyName:
|
|
ProcessAdditionalItems(property.Value);
|
|
break;
|
|
case JsonSchemaConstants.PatternPropertiesPropertyName:
|
|
CurrentSchema.PatternProperties = ProcessProperties(property.Value);
|
|
break;
|
|
case JsonSchemaConstants.RequiredPropertyName:
|
|
CurrentSchema.Required = (bool)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.RequiresPropertyName:
|
|
CurrentSchema.Requires = (string)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.MinimumPropertyName:
|
|
CurrentSchema.Minimum = (double)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.MaximumPropertyName:
|
|
CurrentSchema.Maximum = (double)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.ExclusiveMinimumPropertyName:
|
|
CurrentSchema.ExclusiveMinimum = (bool)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.ExclusiveMaximumPropertyName:
|
|
CurrentSchema.ExclusiveMaximum = (bool)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.MaximumLengthPropertyName:
|
|
CurrentSchema.MaximumLength = (int)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.MinimumLengthPropertyName:
|
|
CurrentSchema.MinimumLength = (int)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.MaximumItemsPropertyName:
|
|
CurrentSchema.MaximumItems = (int)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.MinimumItemsPropertyName:
|
|
CurrentSchema.MinimumItems = (int)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.DivisibleByPropertyName:
|
|
CurrentSchema.DivisibleBy = (double)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.DisallowPropertyName:
|
|
CurrentSchema.Disallow = ProcessType(property.Value);
|
|
break;
|
|
case JsonSchemaConstants.DefaultPropertyName:
|
|
CurrentSchema.Default = property.Value.DeepClone();
|
|
break;
|
|
case JsonSchemaConstants.HiddenPropertyName:
|
|
CurrentSchema.Hidden = (bool)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.ReadOnlyPropertyName:
|
|
CurrentSchema.ReadOnly = (bool)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.FormatPropertyName:
|
|
CurrentSchema.Format = (string)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.PatternPropertyName:
|
|
CurrentSchema.Pattern = (string)property.Value;
|
|
break;
|
|
case JsonSchemaConstants.EnumPropertyName:
|
|
ProcessEnum(property.Value);
|
|
break;
|
|
case JsonSchemaConstants.ExtendsPropertyName:
|
|
ProcessExtends(property.Value);
|
|
break;
|
|
case JsonSchemaConstants.UniqueItemsPropertyName:
|
|
CurrentSchema.UniqueItems = (bool)property.Value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ProcessExtends(JToken token)
|
|
{
|
|
IList<JsonSchema> schemas = new List<JsonSchema>();
|
|
|
|
if (token.Type == JTokenType.Array)
|
|
{
|
|
foreach (JToken schemaObject in token)
|
|
{
|
|
schemas.Add(BuildSchema(schemaObject));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
JsonSchema schema = BuildSchema(token);
|
|
if (schema != null)
|
|
{
|
|
schemas.Add(schema);
|
|
}
|
|
}
|
|
|
|
if (schemas.Count > 0)
|
|
{
|
|
CurrentSchema.Extends = schemas;
|
|
}
|
|
}
|
|
|
|
private void ProcessEnum(JToken token)
|
|
{
|
|
if (token.Type != JTokenType.Array)
|
|
{
|
|
throw JsonException.Create(token, token.Path, "Expected Array token while parsing enum values, got {0}.".FormatWith(CultureInfo.InvariantCulture, token.Type));
|
|
}
|
|
|
|
CurrentSchema.Enum = new List<JToken>();
|
|
|
|
foreach (JToken enumValue in token)
|
|
{
|
|
CurrentSchema.Enum.Add(enumValue.DeepClone());
|
|
}
|
|
}
|
|
|
|
private void ProcessAdditionalProperties(JToken token)
|
|
{
|
|
if (token.Type == JTokenType.Boolean)
|
|
{
|
|
CurrentSchema.AllowAdditionalProperties = (bool)token;
|
|
}
|
|
else
|
|
{
|
|
CurrentSchema.AdditionalProperties = BuildSchema(token);
|
|
}
|
|
}
|
|
|
|
private void ProcessAdditionalItems(JToken token)
|
|
{
|
|
if (token.Type == JTokenType.Boolean)
|
|
{
|
|
CurrentSchema.AllowAdditionalItems = (bool)token;
|
|
}
|
|
else
|
|
{
|
|
CurrentSchema.AdditionalItems = BuildSchema(token);
|
|
}
|
|
}
|
|
|
|
private IDictionary<string, JsonSchema> ProcessProperties(JToken token)
|
|
{
|
|
IDictionary<string, JsonSchema> properties = new Dictionary<string, JsonSchema>();
|
|
|
|
if (token.Type != JTokenType.Object)
|
|
{
|
|
throw JsonException.Create(token, token.Path, "Expected Object token while parsing schema properties, got {0}.".FormatWith(CultureInfo.InvariantCulture, token.Type));
|
|
}
|
|
|
|
foreach (JProperty propertyToken in token)
|
|
{
|
|
if (properties.ContainsKey(propertyToken.Name))
|
|
{
|
|
throw new JsonException("Property {0} has already been defined in schema.".FormatWith(CultureInfo.InvariantCulture, propertyToken.Name));
|
|
}
|
|
|
|
properties.Add(propertyToken.Name, BuildSchema(propertyToken.Value));
|
|
}
|
|
|
|
return properties;
|
|
}
|
|
|
|
private void ProcessItems(JToken token)
|
|
{
|
|
CurrentSchema.Items = new List<JsonSchema>();
|
|
|
|
switch (token.Type)
|
|
{
|
|
case JTokenType.Object:
|
|
CurrentSchema.Items.Add(BuildSchema(token));
|
|
CurrentSchema.PositionalItemsValidation = false;
|
|
break;
|
|
case JTokenType.Array:
|
|
CurrentSchema.PositionalItemsValidation = true;
|
|
foreach (JToken schemaToken in token)
|
|
{
|
|
CurrentSchema.Items.Add(BuildSchema(schemaToken));
|
|
}
|
|
break;
|
|
default:
|
|
throw JsonException.Create(token, token.Path, "Expected array or JSON schema object, got {0}.".FormatWith(CultureInfo.InvariantCulture, token.Type));
|
|
}
|
|
}
|
|
|
|
private JsonSchemaType? ProcessType(JToken token)
|
|
{
|
|
switch (token.Type)
|
|
{
|
|
case JTokenType.Array:
|
|
// ensure type is in blank state before ORing values
|
|
JsonSchemaType? type = JsonSchemaType.None;
|
|
|
|
foreach (JToken typeToken in token)
|
|
{
|
|
if (typeToken.Type != JTokenType.String)
|
|
{
|
|
throw JsonException.Create(typeToken, typeToken.Path, "Expected JSON schema type string token, got {0}.".FormatWith(CultureInfo.InvariantCulture, token.Type));
|
|
}
|
|
|
|
type = type | MapType((string)typeToken);
|
|
}
|
|
|
|
return type;
|
|
case JTokenType.String:
|
|
return MapType((string)token);
|
|
default:
|
|
throw JsonException.Create(token, token.Path, "Expected array or JSON schema type string token, got {0}.".FormatWith(CultureInfo.InvariantCulture, token.Type));
|
|
}
|
|
}
|
|
|
|
internal static JsonSchemaType MapType(string type)
|
|
{
|
|
if (!JsonSchemaConstants.JsonSchemaTypeMapping.TryGetValue(type, out JsonSchemaType mappedType))
|
|
{
|
|
throw new JsonException("Invalid JSON schema type: {0}".FormatWith(CultureInfo.InvariantCulture, type));
|
|
}
|
|
|
|
return mappedType;
|
|
}
|
|
|
|
internal static string MapType(JsonSchemaType type)
|
|
{
|
|
return JsonSchemaConstants.JsonSchemaTypeMapping.Single(kv => kv.Value == type).Key;
|
|
}
|
|
}
|
|
} |