#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));
}
}
}
}