#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.Globalization; using System.ComponentModel; using System.Collections.Generic; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Utilities; using Newtonsoft.Json.Serialization; #if NETFX_CORE using IConvertible = Newtonsoft.Json.Utilities.Convertible; #endif #if NET20 using Newtonsoft.Json.Utilities.LinqBridge; #else using System.Linq; #endif namespace Newtonsoft.Json.Schema { /// /// Generates a from a specified . /// public class JsonSchemaGenerator { /// /// Gets or sets how undefined schemas are handled by the serializer. /// public UndefinedSchemaIdHandling UndefinedSchemaIdHandling { get; set; } private IContractResolver _contractResolver; /// /// Gets or sets the contract resolver. /// /// The contract resolver. public IContractResolver ContractResolver { get { if (_contractResolver == null) return DefaultContractResolver.Instance; return _contractResolver; } set { _contractResolver = value; } } private class TypeSchema { public Type Type { get; private set; } public JsonSchema Schema { get; private set;} public TypeSchema(Type type, JsonSchema schema) { ValidationUtils.ArgumentNotNull(type, "type"); ValidationUtils.ArgumentNotNull(schema, "schema"); Type = type; Schema = schema; } } private JsonSchemaResolver _resolver; private readonly IList _stack = new List(); private JsonSchema _currentSchema; private JsonSchema CurrentSchema { get { return _currentSchema; } } private void Push(TypeSchema typeSchema) { _currentSchema = typeSchema.Schema; _stack.Add(typeSchema); _resolver.LoadedSchemas.Add(typeSchema.Schema); } private TypeSchema Pop() { TypeSchema popped = _stack[_stack.Count - 1]; _stack.RemoveAt(_stack.Count - 1); TypeSchema newValue = _stack.LastOrDefault(); if (newValue != null) { _currentSchema = newValue.Schema; } else { _currentSchema = null; } return popped; } /// /// Generate a from the specified type. /// /// The type to generate a from. /// A generated from the specified type. public JsonSchema Generate(Type type) { return Generate(type, new JsonSchemaResolver(), false); } /// /// Generate a from the specified type. /// /// The type to generate a from. /// The used to resolve schema references. /// A generated from the specified type. public JsonSchema Generate(Type type, JsonSchemaResolver resolver) { return Generate(type, resolver, false); } /// /// Generate a from the specified type. /// /// The type to generate a from. /// Specify whether the generated root will be nullable. /// A generated from the specified type. public JsonSchema Generate(Type type, bool rootSchemaNullable) { return Generate(type, new JsonSchemaResolver(), rootSchemaNullable); } /// /// Generate a from the specified type. /// /// The type to generate a from. /// The used to resolve schema references. /// Specify whether the generated root will be nullable. /// A generated from the specified type. public JsonSchema Generate(Type type, JsonSchemaResolver resolver, bool rootSchemaNullable) { ValidationUtils.ArgumentNotNull(type, "type"); ValidationUtils.ArgumentNotNull(resolver, "resolver"); _resolver = resolver; return GenerateInternal(type, (!rootSchemaNullable) ? Required.Always : Required.Default, false); } private string GetTitle(Type type) { JsonContainerAttribute containerAttribute = JsonTypeReflector.GetJsonContainerAttribute(type); if (containerAttribute != null && !string.IsNullOrEmpty(containerAttribute.Title)) return containerAttribute.Title; return null; } private string GetDescription(Type type) { JsonContainerAttribute containerAttribute = JsonTypeReflector.GetJsonContainerAttribute(type); if (containerAttribute != null && !string.IsNullOrEmpty(containerAttribute.Description)) return containerAttribute.Description; #if !PocketPC && !NETFX_CORE DescriptionAttribute descriptionAttribute = ReflectionUtils.GetAttribute(type); if (descriptionAttribute != null) return descriptionAttribute.Description; #endif return null; } private string GetTypeId(Type type, bool explicitOnly) { JsonContainerAttribute containerAttribute = JsonTypeReflector.GetJsonContainerAttribute(type); if (containerAttribute != null && !string.IsNullOrEmpty(containerAttribute.Id)) return containerAttribute.Id; if (explicitOnly) return null; switch (UndefinedSchemaIdHandling) { case UndefinedSchemaIdHandling.UseTypeName: return type.FullName; case UndefinedSchemaIdHandling.UseAssemblyQualifiedName: return type.AssemblyQualifiedName; default: return null; } } private JsonSchema GenerateInternal(Type type, Required valueRequired, bool required) { ValidationUtils.ArgumentNotNull(type, "type"); string resolvedId = GetTypeId(type, false); string explicitId = GetTypeId(type, true); if (!string.IsNullOrEmpty(resolvedId)) { JsonSchema resolvedSchema = _resolver.GetSchema(resolvedId); if (resolvedSchema != null) { // resolved schema is not null but referencing member allows nulls // change resolved schema to allow nulls. hacky but what are ya gonna do? if (valueRequired != Required.Always && !HasFlag(resolvedSchema.Type, JsonSchemaType.Null)) resolvedSchema.Type |= JsonSchemaType.Null; if (required && resolvedSchema.Required != true) resolvedSchema.Required = true; return resolvedSchema; } } // test for unresolved circular reference if (_stack.Any(tc => tc.Type == type)) { throw new Exception("Unresolved circular reference for type '{0}'. Explicitly define an Id for the type using a JsonObject/JsonArray attribute or automatically generate a type Id using the UndefinedSchemaIdHandling property.".FormatWith(CultureInfo.InvariantCulture, type)); } JsonContract contract = ContractResolver.ResolveContract(type); JsonConverter converter; if ((converter = contract.Converter) != null || (converter = contract.InternalConverter) != null) { JsonSchema converterSchema = converter.GetSchema(); if (converterSchema != null) return converterSchema; } Push(new TypeSchema(type, new JsonSchema())); if (explicitId != null) CurrentSchema.Id = explicitId; if (required) CurrentSchema.Required = true; CurrentSchema.Title = GetTitle(type); CurrentSchema.Description = GetDescription(type); if (converter != null) { // todo: Add GetSchema to JsonConverter and use here? CurrentSchema.Type = JsonSchemaType.Any; } else { switch (contract.ContractType) { case JsonContractType.Object: CurrentSchema.Type = AddNullType(JsonSchemaType.Object, valueRequired); CurrentSchema.Id = GetTypeId(type, false); GenerateObjectSchema(type, (JsonObjectContract) contract); break; case JsonContractType.Array: CurrentSchema.Type = AddNullType(JsonSchemaType.Array, valueRequired); CurrentSchema.Id = GetTypeId(type, false); JsonArrayAttribute arrayAttribute = JsonTypeReflector.GetJsonContainerAttribute(type) as JsonArrayAttribute; bool allowNullItem = (arrayAttribute == null || arrayAttribute.AllowNullItems); Type collectionItemType = ReflectionUtils.GetCollectionItemType(type); if (collectionItemType != null) { CurrentSchema.Items = new List(); CurrentSchema.Items.Add(GenerateInternal(collectionItemType, (!allowNullItem) ? Required.Always : Required.Default, false)); } break; case JsonContractType.Primitive: CurrentSchema.Type = GetJsonSchemaType(type, valueRequired); if (CurrentSchema.Type == JsonSchemaType.Integer && type.IsEnum() && !type.IsDefined(typeof (FlagsAttribute), true)) { CurrentSchema.Enum = new List(); CurrentSchema.Options = new Dictionary(); EnumValues enumValues = EnumUtils.GetNamesAndValues(type); foreach (EnumValue enumValue in enumValues) { JToken value = JToken.FromObject(enumValue.Value); CurrentSchema.Enum.Add(value); CurrentSchema.Options.Add(value, enumValue.Name); } } break; case JsonContractType.String: JsonSchemaType schemaType = (!ReflectionUtils.IsNullable(contract.UnderlyingType)) ? JsonSchemaType.String : AddNullType(JsonSchemaType.String, valueRequired); CurrentSchema.Type = schemaType; break; case JsonContractType.Dictionary: CurrentSchema.Type = AddNullType(JsonSchemaType.Object, valueRequired); Type keyType; Type valueType; ReflectionUtils.GetDictionaryKeyValueTypes(type, out keyType, out valueType); if (keyType != null) { // can be converted to a string if (ConvertUtils.IsConvertible(keyType)) { CurrentSchema.AdditionalProperties = GenerateInternal(valueType, Required.Default, false); } } break; #if !SILVERLIGHT && !PocketPC && !NETFX_CORE case JsonContractType.Serializable: CurrentSchema.Type = AddNullType(JsonSchemaType.Object, valueRequired); CurrentSchema.Id = GetTypeId(type, false); GenerateISerializableContract(type, (JsonISerializableContract) contract); break; #endif #if !(NET35 || NET20 || WINDOWS_PHONE) case JsonContractType.Dynamic: #endif case JsonContractType.Linq: CurrentSchema.Type = JsonSchemaType.Any; break; default: throw new Exception("Unexpected contract type: {0}".FormatWith(CultureInfo.InvariantCulture, contract)); } } return Pop().Schema; } private JsonSchemaType AddNullType(JsonSchemaType type, Required valueRequired) { if (valueRequired != Required.Always) return type | JsonSchemaType.Null; return type; } private bool HasFlag(DefaultValueHandling value, DefaultValueHandling flag) { return ((value & flag) == flag); } private void GenerateObjectSchema(Type type, JsonObjectContract contract) { CurrentSchema.Properties = new Dictionary(); foreach (JsonProperty property in contract.Properties) { if (!property.Ignored) { bool optional = property.NullValueHandling == NullValueHandling.Ignore || HasFlag(property.DefaultValueHandling.GetValueOrDefault(), DefaultValueHandling.Ignore) || property.ShouldSerialize != null || property.GetIsSpecified != null; JsonSchema propertySchema = GenerateInternal(property.PropertyType, property.Required, !optional); if (property.DefaultValue != null) propertySchema.Default = JToken.FromObject(property.DefaultValue); CurrentSchema.Properties.Add(property.PropertyName, propertySchema); } } if (type.IsSealed()) CurrentSchema.AllowAdditionalProperties = false; } #if !SILVERLIGHT && !PocketPC && !NETFX_CORE private void GenerateISerializableContract(Type type, JsonISerializableContract contract) { CurrentSchema.AllowAdditionalProperties = true; } #endif internal static bool HasFlag(JsonSchemaType? value, JsonSchemaType flag) { // default value is Any if (value == null) return true; bool match = ((value & flag) == flag); if (match) return true; // integer is a subset of float if (value == JsonSchemaType.Float && flag == JsonSchemaType.Integer) return true; return false; } private JsonSchemaType GetJsonSchemaType(Type type, Required valueRequired) { JsonSchemaType schemaType = JsonSchemaType.None; if (valueRequired != Required.Always && ReflectionUtils.IsNullable(type)) { schemaType = JsonSchemaType.Null; if (ReflectionUtils.IsNullableType(type)) type = Nullable.GetUnderlyingType(type); } TypeCode typeCode = ConvertUtils.GetTypeCode(type); switch (typeCode) { case TypeCode.Empty: case TypeCode.Object: return schemaType | JsonSchemaType.String; #if !NETFX_CORE case TypeCode.DBNull: return schemaType | JsonSchemaType.Null; #endif case TypeCode.Boolean: return schemaType | JsonSchemaType.Boolean; case TypeCode.Char: return schemaType | JsonSchemaType.String; case TypeCode.SByte: case TypeCode.Byte: case TypeCode.Int16: case TypeCode.UInt16: case TypeCode.Int32: case TypeCode.UInt32: case TypeCode.Int64: case TypeCode.UInt64: return schemaType | JsonSchemaType.Integer; case TypeCode.Single: case TypeCode.Double: case TypeCode.Decimal: return schemaType | JsonSchemaType.Float; // convert to string? case TypeCode.DateTime: return schemaType | JsonSchemaType.String; case TypeCode.String: return schemaType | JsonSchemaType.String; default: throw new Exception("Unexpected type code '{0}' for type '{1}'.".FormatWith(CultureInfo.InvariantCulture, typeCode, type)); } } } }