Json.NET
Code Coverage Statistics for Source File

Newtonsoft.Json\Schema\JsonSchemaGenerator.cs

Symbol Coverage: 96.51% (166 of 172)

Branch Coverage: 94.40% (118 of 125)

Cyclomatic Complexity Avg: 3.59 Max:35

Code Lines: 169


L V Source
1
#region License
2
// Copyright (c) 2007 James Newton-King
3
//
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
11
// conditions:
12
//
13
// The above copyright notice and this permission notice shall be
14
// included in all copies or substantial portions of the Software.
15
//
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.
24
#endregion
25

  
26
using System;
27
using System.Linq;
28
using System.Globalization;
29
using System.ComponentModel;
30
using System.Collections.Generic;
31
using Newtonsoft.Json.Linq;
32
using Newtonsoft.Json.Utilities;
33
using Newtonsoft.Json.Serialization;
34

  
35
namespace Newtonsoft.Json.Schema
36
{
37
  /// <summary>
38
  /// Generates a <see cref="JsonSchema"/> from a specified <see cref="Type"/>.
39
  /// </summary>
40
  public class JsonSchemaGenerator
41
  {
42
    /// <summary>
43
    /// Gets or sets how undefined schemas are handled by the serializer.
44
    /// </summary>
45
    public UndefinedSchemaIdHandling UndefinedSchemaIdHandling { get; set; }
46

  
47
    private IContractResolver _contractResolver;
48
    /// <summary>
49
    /// Gets or sets the contract resolver.
50
    /// </summary>
51
    /// <value>The contract resolver.</value>
52
    public IContractResolver ContractResolver
53
    {
54
      get
55
      {
56
 259
        if (_contractResolver == null)
57
 240
          return DefaultContractResolver.Instance;
58

  
59
 19
        return _contractResolver;
60
 259
      }
61
 2
      set { _contractResolver = value; }
62
    }
63

  
64
    private class TypeSchema
65
    {
66
      public Type Type { get; private set; }
67
      public JsonSchema Schema { get; private set;}
68

  
69
 259
      public TypeSchema(Type type, JsonSchema schema)
70
      {
71
 259
        ValidationUtils.ArgumentNotNull(type, "type");
72
 259
        ValidationUtils.ArgumentNotNull(schema, "schema");
73

  
74
 259
        Type = type;
75
 259
        Schema = schema;
76
 259
      }
77
    }
78

  
79
    private JsonSchemaResolver _resolver;
80
 39
    private IList<TypeSchema> _stack = new List<TypeSchema>();
81
    private JsonSchema _currentSchema;
82

  
83
    private JsonSchema CurrentSchema
84
    {
85
 1362
      get { return _currentSchema; }
86
    }
87

  
88
    private void Push(TypeSchema typeSchema)
89
    {
90
 259
      _currentSchema = typeSchema.Schema;
91
 259
      _stack.Add(typeSchema);
92
 259
      _resolver.LoadedSchemas.Add(typeSchema.Schema);
93
 259
    }
94

  
95
    private TypeSchema Pop()
96
    {
97
 258
      TypeSchema popped = _stack[_stack.Count - 1];
98
 258
      _stack.RemoveAt(_stack.Count - 1);
99
 258
      TypeSchema newValue = _stack.LastOrDefault();
100
 258
      if (newValue != null)
101
      {
102
 218
        _currentSchema = newValue.Schema;
103
      }
104
      else
105
      {
106
 40
        _currentSchema = null;
107
      }
108

  
109
 258
      return popped;
110
 258
    }
111

  
112
    /// <summary>
113
    /// Generate a <see cref="JsonSchema"/> from the specified type.
114
    /// </summary>
115
    /// <param name="type">The type to generate a <see cref="JsonSchema"/> from.</param>
116
    /// <returns>A <see cref="JsonSchema"/> generated from the specified type.</returns>
117
    public JsonSchema Generate(Type type)
118
    {
119
 38
      return Generate(type, new JsonSchemaResolver(), false);
120
 37
    }
121

  
122
    /// <summary>
123
    /// Generate a <see cref="JsonSchema"/> from the specified type.
124
    /// </summary>
125
    /// <param name="type">The type to generate a <see cref="JsonSchema"/> from.</param>
126
    /// <param name="resolver">The <see cref="JsonSchemaResolver"/> used to resolve schema references.</param>
127
    /// <returns>A <see cref="JsonSchema"/> generated from the specified type.</returns>
128
    public JsonSchema Generate(Type type, JsonSchemaResolver resolver)
129
    {
130
 0
      return Generate(type, resolver, false);
131
 0
    }
132

  
133
    /// <summary>
134
    /// Generate a <see cref="JsonSchema"/> from the specified type.
135
    /// </summary>
136
    /// <param name="type">The type to generate a <see cref="JsonSchema"/> from.</param>
137
    /// <param name="rootSchemaNullable">Specify whether the generated root <see cref="JsonSchema"/> will be nullable.</param>
138
    /// <returns>A <see cref="JsonSchema"/> generated from the specified type.</returns>
139
    public JsonSchema Generate(Type type, bool rootSchemaNullable)
140
    {
141
 3
      return Generate(type, new JsonSchemaResolver(), rootSchemaNullable);
142
 3
    }
143

  
144
    /// <summary>
145
    /// Generate a <see cref="JsonSchema"/> from the specified type.
146
    /// </summary>
147
    /// <param name="type">The type to generate a <see cref="JsonSchema"/> from.</param>
148
    /// <param name="resolver">The <see cref="JsonSchemaResolver"/> used to resolve schema references.</param>
149
    /// <param name="rootSchemaNullable">Specify whether the generated root <see cref="JsonSchema"/> will be nullable.</param>
150
    /// <returns>A <see cref="JsonSchema"/> generated from the specified type.</returns>
151
    public JsonSchema Generate(Type type, JsonSchemaResolver resolver, bool rootSchemaNullable)
152
    {
153
 41
      ValidationUtils.ArgumentNotNull(type, "type");
154
 41
      ValidationUtils.ArgumentNotNull(resolver, "resolver");
155

  
156
 41
      _resolver = resolver;
157

  
158
 41
      return GenerateInternal(type, (!rootSchemaNullable) ? Required.Always : Required.Default, false);
159
 40
    }
160

  
161
    private string GetTitle(Type type)
162
    {
163
 259
      JsonContainerAttribute containerAttribute = JsonTypeReflector.GetJsonContainerAttribute(type);
164

  
165
 259
      if (containerAttribute != null && !string.IsNullOrEmpty(containerAttribute.Title))
166
 2
        return containerAttribute.Title;
167

  
168
 257
      return null;
169
 259
    }
170

  
171
    private string GetDescription(Type type)
172
    {
173
 259
      JsonContainerAttribute containerAttribute = JsonTypeReflector.GetJsonContainerAttribute(type);
174

  
175
 259
      if (containerAttribute != null && !string.IsNullOrEmpty(containerAttribute.Description))
176
 2
        return containerAttribute.Description;
177

  
178
#if !PocketPC
179
 257
      DescriptionAttribute descriptionAttribute = ReflectionUtils.GetAttribute<DescriptionAttribute>(type);
180
 257
      if (descriptionAttribute != null)
181
 2
        return descriptionAttribute.Description;
182
#endif
183

  
184
 255
      return null;
185
 259
    }
186

  
187
    private string GetTypeId(Type type, bool explicitOnly)
188
    {
189
 619
      JsonContainerAttribute containerAttribute = JsonTypeReflector.GetJsonContainerAttribute(type);
190

  
191
 619
      if (containerAttribute != null && !string.IsNullOrEmpty(containerAttribute.Id))
192
 16
        return containerAttribute.Id;
193

  
194
 603
      if (explicitOnly)
195
 267
        return null;
196

  
197
 336
      switch (UndefinedSchemaIdHandling)
198
      {
199
        case UndefinedSchemaIdHandling.UseTypeName:
200
 101
          return type.FullName;
201
        case UndefinedSchemaIdHandling.UseAssemblyQualifiedName:
202
 111
          return type.AssemblyQualifiedName;
203
        default:
204
 124
          return null;
205
      }
206
 619
    }
207

  
208
    private JsonSchema GenerateInternal(Type type, Required valueRequired, bool optional)
209
    {
210
 273
      ValidationUtils.ArgumentNotNull(type, "type");
211

  
212
 273
      string resolvedId = GetTypeId(type, false);
213
 273
      string explicitId = GetTypeId(type, true);
214

  
215
 273
      if (!string.IsNullOrEmpty(resolvedId))
216
      {
217
 172
        JsonSchema resolvedSchema = _resolver.GetSchema(resolvedId);
218
 172
        if (resolvedSchema != null)
219
        {
220
          // resolved schema is not null but referencing member allows nulls
221
          // change resolved schema to allow nulls. hacky but what are ya gonna do?
222
 13
          if (valueRequired != Required.Always && !HasFlag(resolvedSchema.Type, JsonSchemaType.Null))
223
 6
            resolvedSchema.Type |= JsonSchemaType.Null;
224
 13
          if (optional && resolvedSchema.Optional != true)
225
 3
            resolvedSchema.Optional = true;
226

  
227
 13
          return resolvedSchema;
228
        }
229
      }
230

  
231
      // test for unresolved circular reference
232
 260
      if (_stack.Any(tc => tc.Type == type))
233
      {
234
 1
        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));
235
      }
236

  
237
 259
      JsonContract contract = ContractResolver.ResolveContract(type);
238
      JsonConverter converter;
239
 259
      if ((converter = contract.Converter) != null || (converter = contract.InternalConverter) != null)
240
      {
241
 1
        JsonSchema converterSchema = converter.GetSchema();
242
 1
        if (converterSchema != null)
243
 0
          return converterSchema;
244
      }
245

  
246
 259
      Push(new TypeSchema(type, new JsonSchema()));
247

  
248
 259
      if (explicitId != null)
249
 4
        CurrentSchema.Id = explicitId;
250

  
251
 259
      if (optional)
252
 4
        CurrentSchema.Optional = true;
253
 259
      CurrentSchema.Title = GetTitle(type);
254
 259
      CurrentSchema.Description = GetDescription(type);
255

  
256
 259
      if (converter != null)
257
      {
258
        // todo: Add GetSchema to JsonConverter and use here?
259
 1
        CurrentSchema.Type = JsonSchemaType.Any;
260
      }
261
 258
      else if (contract is JsonDictionaryContract)
262
      {
263
 1
        CurrentSchema.Type = AddNullType(JsonSchemaType.Object, valueRequired);
264

  
265
        Type keyType;
266
        Type valueType;
267
 1
        ReflectionUtils.GetDictionaryKeyValueTypes(type, out keyType, out valueType);
268

  
269
 1
        if (keyType != null)
270
        {
271
          // can be converted to a string
272
 1
          if (typeof (IConvertible).IsAssignableFrom(keyType))
273
          {
274
 1
            CurrentSchema.AdditionalProperties = GenerateInternal(valueType, Required.Default, false);
275
          }
276
        }
277
      }
278
 257
      else if (contract is JsonArrayContract)
279
      {
280
 33
        CurrentSchema.Type = AddNullType(JsonSchemaType.Array, valueRequired);
281

  
282
 33
        CurrentSchema.Id = GetTypeId(type, false);
283

  
284
 33
        JsonArrayAttribute arrayAttribute = JsonTypeReflector.GetJsonContainerAttribute(type) as JsonArrayAttribute;
285
 33
        bool allowNullItem = (arrayAttribute != null) ? arrayAttribute.AllowNullItems : true;
286

  
287
 33
        Type collectionItemType = ReflectionUtils.GetCollectionItemType(type);
288
 33
        if (collectionItemType != null)
289
        {
290
 33
          CurrentSchema.Items = new List<JsonSchema>();
291
 33
          CurrentSchema.Items.Add(GenerateInternal(collectionItemType, (!allowNullItem) ? Required.Always : Required.Default, false));
292
        }
293
      }
294
 224
      else if (contract is JsonPrimitiveContract)
295
      {
296
 179
        CurrentSchema.Type = GetJsonSchemaType(type, valueRequired);
297

  
298
 179
        if (CurrentSchema.Type == JsonSchemaType.Integer && type.IsEnum && !type.IsDefined(typeof(FlagsAttribute), true))
299
        {
300
 2
          CurrentSchema.Enum = new List<JToken>();
301
 2
          CurrentSchema.Options = new Dictionary<JToken, string>();
302

  
303
 2
          EnumValues<long> enumValues = EnumUtils.GetNamesAndValues<long>(type);
304
 2
          foreach (EnumValue<long> enumValue in enumValues)
305
          {
306
 6
            JToken value = JToken.FromObject(enumValue.Value);
307

  
308
 6
            CurrentSchema.Enum.Add(value);
309
 6
            CurrentSchema.Options.Add(value, enumValue.Name);
310
          }
311
        }
312
      }
313
 45
      else if (contract is JsonObjectContract)
314
      {
315
 39
        CurrentSchema.Type = AddNullType(JsonSchemaType.Object, valueRequired);
316
 39
        CurrentSchema.Id = GetTypeId(type, false);
317
 39
        GenerateObjectSchema(type, (JsonObjectContract)contract);
318
      }
319
#if !SILVERLIGHT && !PocketPC
320
 6
      else if (contract is JsonISerializableContract)
321
      {
322
 1
        CurrentSchema.Type = AddNullType(JsonSchemaType.Object, valueRequired);
323
 1
        CurrentSchema.Id = GetTypeId(type, false);
324
 1
        GenerateISerializableContract(type, (JsonISerializableContract) contract);
325
      }
326
#endif
327
 5
      else if (contract is JsonStringContract)
328
      {
329
 4
        JsonSchemaType schemaType = (!ReflectionUtils.IsNullable(contract.UnderlyingType))
330
 4
                                      ? JsonSchemaType.String
331
 4
                                      : AddNullType(JsonSchemaType.String, valueRequired);
332

  
333
 4
        CurrentSchema.Type = schemaType;
334
      }
335
 1
      else if (contract is JsonLinqContract)
336
      {
337
 1
        CurrentSchema.Type = JsonSchemaType.Any;
338
      }
339
      else
340
      {
341
 0
        throw new Exception("Unexpected contract type: {0}".FormatWith(CultureInfo.InvariantCulture, contract));
342
      }
343

  
344
 258
      return Pop().Schema;
345
 271
    }
346

  
347
    private JsonSchemaType AddNullType(JsonSchemaType type, Required valueRequired)
348
    {
349
 75
      if (valueRequired != Required.Always)
350
 44
        return type | JsonSchemaType.Null;
351

  
352
 31
      return type;
353
 75
    }
354

  
355
    private void GenerateObjectSchema(Type type, JsonObjectContract contract)
356
    {
357
 39
      CurrentSchema.Properties = new Dictionary<string, JsonSchema>();
358
 39
      foreach (JsonProperty property in contract.Properties)
359
      {
360
 201
        if (!property.Ignored)
361
        {
362
 198
          bool optional = property.NullValueHandling == NullValueHandling.Ignore ||
363
 198
                          property.DefaultValueHandling == DefaultValueHandling.Ignore ||
364
 198
                          property.ShouldSerialize != null;
365

  
366
 198
          JsonSchema propertySchema = GenerateInternal(property.PropertyType, property.Required, optional);
367

  
368
 197
          if (property.DefaultValue != null)
369
 6
            propertySchema.Default = JToken.FromObject(property.DefaultValue);
370

  
371
 197
          CurrentSchema.Properties.Add(property.PropertyName, propertySchema);
372
        }
373
      }
374

  
375
 38
      if (type.IsSealed)
376
 5
        CurrentSchema.AllowAdditionalProperties = false;
377
 38
    }
378

  
379
#if !SILVERLIGHT && !PocketPC
380
    private void GenerateISerializableContract(Type type, JsonISerializableContract contract)
381
    {
382
 1
      CurrentSchema.AllowAdditionalProperties = true;
383
 1
    }
384
#endif
385

  
386
    internal static bool HasFlag(JsonSchemaType? value, JsonSchemaType flag)
387
    {
388
      // default value is Any
389
 340
      if (value == null)
390
 0
        return true;
391

  
392
 340
      return ((value & flag) == flag);
393
 340
    }
394

  
395
    private JsonSchemaType GetJsonSchemaType(Type type, Required valueRequired)
396
    {
397
 179
      JsonSchemaType schemaType = JsonSchemaType.None;
398
 179
      if (valueRequired != Required.Always && ReflectionUtils.IsNullable(type))
399
      {
400
 76
        schemaType = JsonSchemaType.Null;
401
 76
        if (ReflectionUtils.IsNullableType(type))
402
 5
          type = Nullable.GetUnderlyingType(type);
403
      }
404

  
405
 179
      TypeCode typeCode = Type.GetTypeCode(type);
406

  
407
 179
      switch (typeCode)
408
      {
409
        case TypeCode.Empty:
410
        case TypeCode.Object:
411
 1
          return schemaType | JsonSchemaType.String;
412
        case TypeCode.DBNull:
413
 2
          return schemaType | JsonSchemaType.Null;
414
        case TypeCode.Boolean:
415
 8
          return schemaType | JsonSchemaType.Boolean;
416
        case TypeCode.Char:
417
 5
          return schemaType | JsonSchemaType.String;
418
        case TypeCode.SByte:
419
        case TypeCode.Byte:
420
        case TypeCode.Int16:
421
        case TypeCode.UInt16:
422
        case TypeCode.Int32:
423
        case TypeCode.UInt32:
424
        case TypeCode.Int64:
425
        case TypeCode.UInt64:
426
 48
          return schemaType | JsonSchemaType.Integer;
427
        case TypeCode.Single:
428
        case TypeCode.Double:
429
        case TypeCode.Decimal:
430
 15
          return schemaType | JsonSchemaType.Float;
431
        // convert to string?
432
        case TypeCode.DateTime:
433
 23
          return schemaType | JsonSchemaType.String;
434
        case TypeCode.String:
435
 77
          return schemaType | JsonSchemaType.String;
436
        default:
437
 0
          throw new Exception("Unexpected type code '{0}' for type '{1}'.".FormatWith(CultureInfo.InvariantCulture, typeCode, type));
438
      }
439
 179
    }
440
  }
441
}