#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; #nullable disable 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 _stack; private readonly JsonSchemaResolver _resolver; private readonly IDictionary _documentSchemas; private JsonSchema _currentSchema; private JObject _rootSchema; public JsonSchemaBuilder(JsonSchemaResolver resolver) { _stack = new List(); _documentSchemas = new Dictionary(); _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 patternProperty in schema.PatternProperties.ToList()) { schema.PatternProperties[patternProperty.Key] = ResolveReferences(patternProperty.Value); } } if (schema.Properties != null) { foreach (KeyValuePair 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 (!StringUtils.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 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 schemas = new List(); 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(); 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 ProcessProperties(JToken token) { IDictionary properties = new Dictionary(); 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(); 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; } } }