2
// Copyright (c) 2007 James Newton-King
4
// Permission is hereby granted, free of charge, to any person
5
// obtaining a copy of this software and associated documentation
6
// files (the "Software"), to deal in the Software without
7
// restriction, including without limitation the rights to use,
8
// copy, modify, merge, publish, distribute, sublicense, and/or sell
9
// copies of the Software, and to permit persons to whom the
10
// Software is furnished to do so, subject to the following
13
// The above copyright notice and this permission notice shall be
14
// included in all copies or substantial portions of the Software.
16
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
// OTHER DEALINGS IN THE SOFTWARE.
27
using System.Globalization;
28
using System.ComponentModel;
29
using System.Collections.Generic;
30
using Newtonsoft.Json.Linq;
31
using Newtonsoft.Json.Utilities;
32
using Newtonsoft.Json.Serialization;
34
using IConvertible = Newtonsoft.Json.Utilities.Convertible;
37
using Newtonsoft.Json.Utilities.LinqBridge;
42
namespace Newtonsoft.Json.Schema
45
/// Generates a <see cref="JsonSchema"/> from a specified <see cref="Type"/>.
47
public class JsonSchemaGenerator
50
/// Gets or sets how undefined schemas are handled by the serializer.
52
public UndefinedSchemaIdHandling UndefinedSchemaIdHandling { get; set; }
54
private IContractResolver _contractResolver;
56
/// Gets or sets the contract resolver.
58
/// <value>The contract resolver.</value>
59
public IContractResolver ContractResolver
63
if (_contractResolver == null)
64
return DefaultContractResolver.Instance;
66
return _contractResolver;
68
set { _contractResolver = value; }
71
private class TypeSchema
73
public Type Type { get; private set; }
74
public JsonSchema Schema { get; private set;}
76
public TypeSchema(Type type, JsonSchema schema)
78
ValidationUtils.ArgumentNotNull(type, "type");
79
ValidationUtils.ArgumentNotNull(schema, "schema");
86
private JsonSchemaResolver _resolver;
87
private readonly IList<TypeSchema> _stack = new List<TypeSchema>();
88
private JsonSchema _currentSchema;
90
private JsonSchema CurrentSchema
92
get { return _currentSchema; }
95
private void Push(TypeSchema typeSchema)
97
_currentSchema = typeSchema.Schema;
98
_stack.Add(typeSchema);
99
_resolver.LoadedSchemas.Add(typeSchema.Schema);
102
private TypeSchema Pop()
104
TypeSchema popped = _stack[_stack.Count - 1];
105
_stack.RemoveAt(_stack.Count - 1);
106
TypeSchema newValue = _stack.LastOrDefault();
107
if (newValue != null)
109
_currentSchema = newValue.Schema;
113
_currentSchema = null;
120
/// Generate a <see cref="JsonSchema"/> from the specified type.
122
/// <param name="type">The type to generate a <see cref="JsonSchema"/> from.</param>
123
/// <returns>A <see cref="JsonSchema"/> generated from the specified type.</returns>
124
public JsonSchema Generate(Type type)
126
return Generate(type, new JsonSchemaResolver(), false);
130
/// Generate a <see cref="JsonSchema"/> from the specified type.
132
/// <param name="type">The type to generate a <see cref="JsonSchema"/> from.</param>
133
/// <param name="resolver">The <see cref="JsonSchemaResolver"/> used to resolve schema references.</param>
134
/// <returns>A <see cref="JsonSchema"/> generated from the specified type.</returns>
135
public JsonSchema Generate(Type type, JsonSchemaResolver resolver)
137
return Generate(type, resolver, false);
141
/// Generate a <see cref="JsonSchema"/> from the specified type.
143
/// <param name="type">The type to generate a <see cref="JsonSchema"/> from.</param>
144
/// <param name="rootSchemaNullable">Specify whether the generated root <see cref="JsonSchema"/> will be nullable.</param>
145
/// <returns>A <see cref="JsonSchema"/> generated from the specified type.</returns>
146
public JsonSchema Generate(Type type, bool rootSchemaNullable)
148
return Generate(type, new JsonSchemaResolver(), rootSchemaNullable);
152
/// Generate a <see cref="JsonSchema"/> from the specified type.
154
/// <param name="type">The type to generate a <see cref="JsonSchema"/> from.</param>
155
/// <param name="resolver">The <see cref="JsonSchemaResolver"/> used to resolve schema references.</param>
156
/// <param name="rootSchemaNullable">Specify whether the generated root <see cref="JsonSchema"/> will be nullable.</param>
157
/// <returns>A <see cref="JsonSchema"/> generated from the specified type.</returns>
158
public JsonSchema Generate(Type type, JsonSchemaResolver resolver, bool rootSchemaNullable)
160
ValidationUtils.ArgumentNotNull(type, "type");
161
ValidationUtils.ArgumentNotNull(resolver, "resolver");
163
_resolver = resolver;
165
return GenerateInternal(type, (!rootSchemaNullable) ? Required.Always : Required.Default, false);
168
private string GetTitle(Type type)
170
JsonContainerAttribute containerAttribute = JsonTypeReflector.GetJsonContainerAttribute(type);
172
if (containerAttribute != null && !string.IsNullOrEmpty(containerAttribute.Title))
173
return containerAttribute.Title;
178
private string GetDescription(Type type)
180
JsonContainerAttribute containerAttribute = JsonTypeReflector.GetJsonContainerAttribute(type);
182
if (containerAttribute != null && !string.IsNullOrEmpty(containerAttribute.Description))
183
return containerAttribute.Description;
185
#if !(NETFX_CORE || PORTABLE)
186
DescriptionAttribute descriptionAttribute = ReflectionUtils.GetAttribute<DescriptionAttribute>(type);
187
if (descriptionAttribute != null)
188
return descriptionAttribute.Description;
194
private string GetTypeId(Type type, bool explicitOnly)
196
JsonContainerAttribute containerAttribute = JsonTypeReflector.GetJsonContainerAttribute(type);
198
if (containerAttribute != null && !string.IsNullOrEmpty(containerAttribute.Id))
199
return containerAttribute.Id;
204
switch (UndefinedSchemaIdHandling)
206
case UndefinedSchemaIdHandling.UseTypeName:
207
return type.FullName;
208
case UndefinedSchemaIdHandling.UseAssemblyQualifiedName:
209
return type.AssemblyQualifiedName;
215
private JsonSchema GenerateInternal(Type type, Required valueRequired, bool required)
217
ValidationUtils.ArgumentNotNull(type, "type");
219
string resolvedId = GetTypeId(type, false);
220
string explicitId = GetTypeId(type, true);
222
if (!string.IsNullOrEmpty(resolvedId))
224
JsonSchema resolvedSchema = _resolver.GetSchema(resolvedId);
225
if (resolvedSchema != null)
227
// resolved schema is not null but referencing member allows nulls
228
// change resolved schema to allow nulls. hacky but what are ya gonna do?
229
if (valueRequired != Required.Always && !HasFlag(resolvedSchema.Type, JsonSchemaType.Null))
230
resolvedSchema.Type |= JsonSchemaType.Null;
231
if (required && resolvedSchema.Required != true)
232
resolvedSchema.Required = true;
234
return resolvedSchema;
238
// test for unresolved circular reference
239
if (_stack.Any(tc => tc.Type == type))
241
throw new JsonException("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));
244
JsonContract contract = ContractResolver.ResolveContract(type);
245
JsonConverter converter;
246
if ((converter = contract.Converter) != null || (converter = contract.InternalConverter) != null)
248
JsonSchema converterSchema = converter.GetSchema();
249
if (converterSchema != null)
250
return converterSchema;
253
Push(new TypeSchema(type, new JsonSchema()));
255
if (explicitId != null)
256
CurrentSchema.Id = explicitId;
259
CurrentSchema.Required = true;
260
CurrentSchema.Title = GetTitle(type);
261
CurrentSchema.Description = GetDescription(type);
263
if (converter != null)
265
// todo: Add GetSchema to JsonConverter and use here?
266
CurrentSchema.Type = JsonSchemaType.Any;
270
switch (contract.ContractType)
272
case JsonContractType.Object:
273
CurrentSchema.Type = AddNullType(JsonSchemaType.Object, valueRequired);
274
CurrentSchema.Id = GetTypeId(type, false);
275
GenerateObjectSchema(type, (JsonObjectContract) contract);
277
case JsonContractType.Array:
278
CurrentSchema.Type = AddNullType(JsonSchemaType.Array, valueRequired);
280
CurrentSchema.Id = GetTypeId(type, false);
282
JsonArrayAttribute arrayAttribute = JsonTypeReflector.GetJsonContainerAttribute(type) as JsonArrayAttribute;
283
bool allowNullItem = (arrayAttribute == null || arrayAttribute.AllowNullItems);
285
Type collectionItemType = ReflectionUtils.GetCollectionItemType(type);
286
if (collectionItemType != null)
288
CurrentSchema.Items = new List<JsonSchema>();
289
CurrentSchema.Items.Add(GenerateInternal(collectionItemType, (!allowNullItem) ? Required.Always : Required.Default, false));
292
case JsonContractType.Primitive:
293
CurrentSchema.Type = GetJsonSchemaType(type, valueRequired);
295
if (CurrentSchema.Type == JsonSchemaType.Integer && type.IsEnum() && !type.IsDefined(typeof (FlagsAttribute), true))
297
CurrentSchema.Enum = new List<JToken>();
298
CurrentSchema.Options = new Dictionary<JToken, string>();
300
EnumValues<long> enumValues = EnumUtils.GetNamesAndValues<long>(type);
301
foreach (EnumValue<long> enumValue in enumValues)
303
JToken value = JToken.FromObject(enumValue.Value);
305
CurrentSchema.Enum.Add(value);
306
CurrentSchema.Options.Add(value, enumValue.Name);
310
case JsonContractType.String:
311
JsonSchemaType schemaType = (!ReflectionUtils.IsNullable(contract.UnderlyingType))
312
? JsonSchemaType.String
313
: AddNullType(JsonSchemaType.String, valueRequired);
315
CurrentSchema.Type = schemaType;
317
case JsonContractType.Dictionary:
318
CurrentSchema.Type = AddNullType(JsonSchemaType.Object, valueRequired);
322
ReflectionUtils.GetDictionaryKeyValueTypes(type, out keyType, out valueType);
326
// can be converted to a string
327
if (ConvertUtils.IsConvertible(keyType))
329
CurrentSchema.AdditionalProperties = GenerateInternal(valueType, Required.Default, false);
333
#if !(SILVERLIGHT || NETFX_CORE || PORTABLE)
334
case JsonContractType.Serializable:
335
CurrentSchema.Type = AddNullType(JsonSchemaType.Object, valueRequired);
336
CurrentSchema.Id = GetTypeId(type, false);
337
GenerateISerializableContract(type, (JsonISerializableContract) contract);
340
#if !(NET35 || NET20 || WINDOWS_PHONE || PORTABLE)
341
case JsonContractType.Dynamic:
343
case JsonContractType.Linq:
344
CurrentSchema.Type = JsonSchemaType.Any;
347
throw new JsonException("Unexpected contract type: {0}".FormatWith(CultureInfo.InvariantCulture, contract));
354
private JsonSchemaType AddNullType(JsonSchemaType type, Required valueRequired)
356
if (valueRequired != Required.Always)
357
return type | JsonSchemaType.Null;
362
private bool HasFlag(DefaultValueHandling value, DefaultValueHandling flag)
364
return ((value & flag) == flag);
367
private void GenerateObjectSchema(Type type, JsonObjectContract contract)
369
CurrentSchema.Properties = new Dictionary<string, JsonSchema>();
370
foreach (JsonProperty property in contract.Properties)
372
if (!property.Ignored)
374
bool optional = property.NullValueHandling == NullValueHandling.Ignore ||
375
HasFlag(property.DefaultValueHandling.GetValueOrDefault(), DefaultValueHandling.Ignore) ||
376
property.ShouldSerialize != null ||
377
property.GetIsSpecified != null;
379
JsonSchema propertySchema = GenerateInternal(property.PropertyType, property.Required, !optional);
381
if (property.DefaultValue != null)
382
propertySchema.Default = JToken.FromObject(property.DefaultValue);
384
CurrentSchema.Properties.Add(property.PropertyName, propertySchema);
389
CurrentSchema.AllowAdditionalProperties = false;
392
#if !(SILVERLIGHT || NETFX_CORE || PORTABLE)
393
private void GenerateISerializableContract(Type type, JsonISerializableContract contract)
395
CurrentSchema.AllowAdditionalProperties = true;
399
internal static bool HasFlag(JsonSchemaType? value, JsonSchemaType flag)
401
// default value is Any
405
bool match = ((value & flag) == flag);
409
// integer is a subset of float
410
if (value == JsonSchemaType.Float && flag == JsonSchemaType.Integer)
416
private JsonSchemaType GetJsonSchemaType(Type type, Required valueRequired)
418
JsonSchemaType schemaType = JsonSchemaType.None;
419
if (valueRequired != Required.Always && ReflectionUtils.IsNullable(type))
421
schemaType = JsonSchemaType.Null;
422
if (ReflectionUtils.IsNullableType(type))
423
type = Nullable.GetUnderlyingType(type);
426
TypeCode typeCode = ConvertUtils.GetTypeCode(type);
431
case TypeCode.Object:
432
return schemaType | JsonSchemaType.String;
433
#if !(NETFX_CORE || PORTABLE)
434
case TypeCode.DBNull:
435
return schemaType | JsonSchemaType.Null;
437
case TypeCode.Boolean:
438
return schemaType | JsonSchemaType.Boolean;
440
return schemaType | JsonSchemaType.String;
444
case TypeCode.UInt16:
446
case TypeCode.UInt32:
448
case TypeCode.UInt64:
449
return schemaType | JsonSchemaType.Integer;
450
case TypeCode.Single:
451
case TypeCode.Double:
452
case TypeCode.Decimal:
453
return schemaType | JsonSchemaType.Float;
454
// convert to string?
455
case TypeCode.DateTime:
456
return schemaType | JsonSchemaType.String;
457
case TypeCode.String:
458
return schemaType | JsonSchemaType.String;
460
throw new JsonException("Unexpected type code '{0}' for type '{1}'.".FormatWith(CultureInfo.InvariantCulture, typeCode, type));